From 8277999b16894a5c54035d5b025d7c65bf9b35ec Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Sat, 17 Dec 2016 13:39:47 -0500 Subject: [PATCH 001/140] add imageview into simpleexoplayerview to display subtitles that are image based --- .../android/exoplayer2/text/ImageCue.java | 15 +++ .../exoplayer2/ui/SimpleExoPlayerView.java | 94 +++++++++++++++++-- .../res/layout/exo_simple_player_view.xml | 6 ++ 3 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java b/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java new file mode 100644 index 0000000000..6274493780 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java @@ -0,0 +1,15 @@ +package com.google.android.exoplayer2.text; + +import android.graphics.Bitmap; + +public class ImageCue extends Cue { + + public ImageCue() { super(""); } + + public Bitmap getBitmap() { return null; } + public int getX() { return 0; } + public int getY() { return 0; } + public int getWidth() { return 0; } + public int getHeight() { return 0; } + public boolean isForcedSubtitle() { return false; } +} diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 97c564a3a6..e01956c803 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -40,6 +40,7 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.ImageCue; import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -171,6 +172,7 @@ public final class SimpleExoPlayerView extends FrameLayout { private final View surfaceView; private final ImageView artworkView; private final SubtitleView subtitleView; + private final ImageView subtitleImageView; private final PlaybackControlView controller; private final ComponentListener componentListener; private final FrameLayout overlayFrameLayout; @@ -178,6 +180,7 @@ public final class SimpleExoPlayerView extends FrameLayout { private SimpleExoPlayer player; private boolean useController; private boolean useArtwork; + private boolean subtitlesEnabled = false; private int controllerShowTimeoutMs; public SimpleExoPlayerView(Context context) { @@ -253,6 +256,8 @@ public final class SimpleExoPlayerView extends FrameLayout { subtitleView.setUserDefaultTextSize(); } + subtitleImageView = (ImageView) findViewById(R.id.exo_subtitles_image); + // Playback control view. View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); if (controllerPlaceholder != null) { @@ -523,14 +528,26 @@ public final class SimpleExoPlayerView extends FrameLayout { return; } TrackSelectionArray selections = player.getCurrentTrackSelections(); + boolean quickExit = false; for (int i = 0; i < selections.length; i++) { - if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { - // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in - // onRenderedFirstFrame(). - hideArtwork(); - return; + switch(player.getRendererType(i)) { + case C.TRACK_TYPE_VIDEO: + if (selections.get(i) != null) { + // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in + // onRenderedFirstFrame(). + + hideArtwork(); + quickExit = true; + } + break; + case C.TRACK_TYPE_TEXT: + if (selections.get(i) != null) + subtitlesEnabled = true; + break; } } + if (quickExit) + return; // Video disabled so the shutter must be closed. if (shutterView != null) { shutterView.setVisibility(VISIBLE); @@ -597,20 +614,85 @@ public final class SimpleExoPlayerView extends FrameLayout { private final class ComponentListener implements SimpleExoPlayer.VideoListener, TextRenderer.Output, ExoPlayer.EventListener { + + private int sourceWidth = 0; + private int sourceHeight = 0; + // TextRenderer.Output implementation @Override public void onCues(List cues) { - if (subtitleView != null) { + + boolean skipNormalCues = false; + if (subtitleImageView != null) { + + final ImageCue cue = (cues != null && !cues.isEmpty() && cues.get(0) instanceof ImageCue) ? (ImageCue) cues.get(0) : null; + skipNormalCues = (cue != null); + if (cue == null || (!subtitlesEnabled && !cue.isForcedSubtitle())) { + subtitleImageView.setImageBitmap(null); + subtitleImageView.setVisibility(View.INVISIBLE); + } + else { + handleImageCue(cue); + } + } + if (!skipNormalCues && subtitleView != null) { subtitleView.onCues(cues); } } + private void handleImageCue(ImageCue cue) { + Bitmap bitmap = cue.getBitmap(); + if (bitmap != null && surfaceView != null) { + int surfaceAnchorX = (int) surfaceView.getX(); + int surfaceAnchorY = (int) surfaceView.getY(); + int surfaceWidth = surfaceView.getWidth(); + int surfaceHeight = surfaceView.getHeight(); + int subAnchorX = cue.getX(); + int subAnchorY = cue.getY(); + int subScaleWidth = cue.getWidth(); + int subScaleHeight = cue.getHeight(); + + // they should change together as we keep the aspect ratio + if ((surfaceHeight != sourceHeight || surfaceWidth != sourceWidth) + && sourceHeight > 0 && sourceWidth > 0) { + double scale; + if (surfaceWidth != sourceWidth) + scale = (double) surfaceWidth / (double) sourceWidth; + else + scale = (double) surfaceHeight / (double) sourceHeight; + subScaleHeight = (int) (scale * subScaleHeight); + subScaleWidth = (int) (scale * subScaleWidth); + } + if (surfaceAnchorX != 0) + subAnchorX += surfaceAnchorX; + if (subAnchorY != 0) + subAnchorY += surfaceAnchorY; + + ViewGroup.LayoutParams params = subtitleImageView.getLayoutParams(); + params.width = subScaleWidth; + params.height = subScaleHeight; + subtitleImageView.setX(subAnchorX); + subtitleImageView.setY(subAnchorY); + subtitleImageView.setLayoutParams(params); + subtitleImageView.setImageBitmap(bitmap); + subtitleImageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + subtitleImageView.setVisibility(View.VISIBLE); + } + else { + subtitleImageView.setImageBitmap(null); + subtitleImageView.setVisibility(View.INVISIBLE); + } + } + // SimpleExoPlayer.VideoListener implementation @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { + + sourceWidth = width; + sourceHeight = height; if (contentFrame != null) { float aspectRatio = height == 0 ? 1 : (width * pixelWidthHeightRatio) / height; contentFrame.setAspectRatio(aspectRatio); diff --git a/library/src/main/res/layout/exo_simple_player_view.xml b/library/src/main/res/layout/exo_simple_player_view.xml index 1f59b7796d..bfd5638713 100644 --- a/library/src/main/res/layout/exo_simple_player_view.xml +++ b/library/src/main/res/layout/exo_simple_player_view.xml @@ -36,6 +36,12 @@ android:layout_width="match_parent" android:layout_height="match_parent"/> + + Date: Sat, 17 Dec 2016 14:43:07 -0500 Subject: [PATCH 002/140] less interfacy.. more classy --- .../android/exoplayer2/text/ImageCue.java | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java b/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java index 6274493780..2dc0c97238 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java @@ -4,12 +4,30 @@ import android.graphics.Bitmap; public class ImageCue extends Cue { - public ImageCue() { super(""); } + final private long start_display_time; + final private int x; + final private int y; + final private int height; + final private int width; + final private Bitmap bitmap; + final private boolean isForced; - public Bitmap getBitmap() { return null; } - public int getX() { return 0; } - public int getY() { return 0; } - public int getWidth() { return 0; } - public int getHeight() { return 0; } - public boolean isForcedSubtitle() { return false; } + public ImageCue(Bitmap bitmap, long start_display_time, int x, int y, int width, int height, boolean isForced) { + super(""); + this.bitmap = bitmap; + this.start_display_time = start_display_time; + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.isForced = isForced; + } + + public long getStartDisplayTime() { return start_display_time; } + public Bitmap getBitmap() { return bitmap; } + public int getX() { return x; } + public int getY() { return y; } + public int getWidth() { return width; } + public int getHeight() { return height; } + public boolean isForcedSubtitle() { return isForced; } } From 47d8b7ff164ae2b162913ec9fea92bcc7fa58839 Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Sun, 18 Dec 2016 22:37:37 -0500 Subject: [PATCH 003/140] get source dimensions from plane stored in subs --- .../android/exoplayer2/text/ImageCue.java | 22 +++++++++++++------ .../exoplayer2/ui/SimpleExoPlayerView.java | 12 ++++------ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java b/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java index 2dc0c97238..b5906cf41d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java @@ -7,19 +7,25 @@ public class ImageCue extends Cue { final private long start_display_time; final private int x; final private int y; - final private int height; - final private int width; + final private int bitmap_height; + final private int bitmap_width; + final private int plane_height; + final private int plane_width; final private Bitmap bitmap; final private boolean isForced; - public ImageCue(Bitmap bitmap, long start_display_time, int x, int y, int width, int height, boolean isForced) { + public ImageCue(Bitmap bitmap, long start_display_time, + int x, int y, int bitmap_width, int bitmap_height, boolean isForced, + int plane_width, int plane_height) { super(""); this.bitmap = bitmap; this.start_display_time = start_display_time; this.x = x; this.y = y; - this.width = width; - this.height = height; + this.bitmap_width = bitmap_width; + this.bitmap_height = bitmap_height; + this.plane_width = plane_width; + this.plane_height = plane_height; this.isForced = isForced; } @@ -27,7 +33,9 @@ public class ImageCue extends Cue { public Bitmap getBitmap() { return bitmap; } public int getX() { return x; } public int getY() { return y; } - public int getWidth() { return width; } - public int getHeight() { return height; } + public int getBitmapWidth() { return bitmap_width; } + public int getBitmapHeight() { return bitmap_height; } + public int getPlaneWidth() { return plane_width; } + public int getPlaneHeight() { return plane_height; } public boolean isForcedSubtitle() { return isForced; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index e01956c803..ffe2ca31e0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -614,10 +614,6 @@ public final class SimpleExoPlayerView extends FrameLayout { private final class ComponentListener implements SimpleExoPlayer.VideoListener, TextRenderer.Output, ExoPlayer.EventListener { - - private int sourceWidth = 0; - private int sourceHeight = 0; - // TextRenderer.Output implementation @Override @@ -648,10 +644,12 @@ public final class SimpleExoPlayerView extends FrameLayout { int surfaceAnchorY = (int) surfaceView.getY(); int surfaceWidth = surfaceView.getWidth(); int surfaceHeight = surfaceView.getHeight(); + int sourceWidth = cue.getPlaneWidth(); + int sourceHeight = cue.getPlaneHeight(); int subAnchorX = cue.getX(); int subAnchorY = cue.getY(); - int subScaleWidth = cue.getWidth(); - int subScaleHeight = cue.getHeight(); + int subScaleWidth = cue.getBitmapWidth(); + int subScaleHeight = cue.getBitmapHeight(); // they should change together as we keep the aspect ratio if ((surfaceHeight != sourceHeight || surfaceWidth != sourceWidth) @@ -691,8 +689,6 @@ public final class SimpleExoPlayerView extends FrameLayout { public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - sourceWidth = width; - sourceHeight = height; if (contentFrame != null) { float aspectRatio = height == 0 ? 1 : (width * pixelWidthHeightRatio) / height; contentFrame.setAspectRatio(aspectRatio); From 44b21f2e3b65787aa6b5b547d5ac67e97a924a61 Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Tue, 20 Dec 2016 22:49:18 -0500 Subject: [PATCH 004/140] remove imagecue and add bitmap to cue with size_height, change to painter for displaying --- .../google/android/exoplayer2/text/Cue.java | 45 ++++++++ .../android/exoplayer2/text/ImageCue.java | 41 ------- .../exoplayer2/ui/SimpleExoPlayerView.java | 65 +---------- .../exoplayer2/ui/SubtitlePainter.java | 104 +++++++++++++----- .../res/layout/exo_simple_player_view.xml | 6 - 5 files changed, 125 insertions(+), 136 deletions(-) delete mode 100644 library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/src/main/java/com/google/android/exoplayer2/text/Cue.java index 93b1dc1d9a..23cce573f7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text; +import android.graphics.Bitmap; import android.support.annotation.IntDef; import android.text.Layout.Alignment; import java.lang.annotation.Retention; @@ -78,6 +79,10 @@ public class Cue { * The alignment of the cue text within the cue box, or null if the alignment is undefined. */ public final Alignment textAlignment; + /** + * The cue image. + */ + public final Bitmap bitmap; /** * The position of the {@link #lineAnchor} of the cue box within the viewport in the direction * orthogonal to the writing direction, or {@link #DIMEN_UNSET}. When set, the interpretation of @@ -85,6 +90,8 @@ public class Cue { *

* For horizontal text and {@link #lineType} equal to {@link #LINE_TYPE_FRACTION}, this is the * fractional vertical position relative to the top of the viewport. + *

+ * If {@link #bitmap} is not null then this value is used to indicate the top position */ public final float line; /** @@ -119,6 +126,8 @@ public class Cue { * For horizontal text, this is the horizontal position relative to the left of the viewport. Note * that positioning is relative to the left of the viewport even in the case of right-to-left * text. + *

+ * If {@link #bitmap} is not null then this value is used to indicate the left position */ public final float position; /** @@ -134,9 +143,25 @@ public class Cue { /** * The size of the cue box in the writing direction specified as a fraction of the viewport size * in that direction, or {@link #DIMEN_UNSET}. + *

+ * If {@link #bitmap} is not null then this value is used to indicate the width */ public final float size; + /** + * The height size of the cue box when a {@link #bitmap} is set specified as a fraction of the + * viewport size in that direction, or {@link #DIMEN_UNSET}. + */ + public final float size_height; + + /** + * + */ + public Cue(Bitmap bitmap, float left, float top, float size, float size_height) { + this(null, null, top, LINE_TYPE_FRACTION, TYPE_UNSET, left, TYPE_UNSET, size, size_height, + bitmap); + } + /** * Constructs a 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}. @@ -159,6 +184,24 @@ public class Cue { */ public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size) { + this(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size, + DIMEN_UNSET, null); + } + /** + * @param text See {@link #text}. + * @param textAlignment See {@link #textAlignment}. + * @param line See {@link #line}. + * @param lineType See {@link #lineType}. + * @param lineAnchor See {@link #lineAnchor}. + * @param position See {@link #position}. + * @param positionAnchor See {@link #positionAnchor}. + * @param size See {@link #size}. + * @param size_height See {@link #size_height}. + * @param bitmap See {@link #bitmap}. + */ + private Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, + @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, + float size_height, Bitmap bitmap) { this.text = text; this.textAlignment = textAlignment; this.line = line; @@ -167,6 +210,8 @@ public class Cue { this.position = position; this.positionAnchor = positionAnchor; this.size = size; + this.size_height = size_height; + this.bitmap = bitmap; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java b/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java deleted file mode 100644 index b5906cf41d..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.google.android.exoplayer2.text; - -import android.graphics.Bitmap; - -public class ImageCue extends Cue { - - final private long start_display_time; - final private int x; - final private int y; - final private int bitmap_height; - final private int bitmap_width; - final private int plane_height; - final private int plane_width; - final private Bitmap bitmap; - final private boolean isForced; - - public ImageCue(Bitmap bitmap, long start_display_time, - int x, int y, int bitmap_width, int bitmap_height, boolean isForced, - int plane_width, int plane_height) { - super(""); - this.bitmap = bitmap; - this.start_display_time = start_display_time; - this.x = x; - this.y = y; - this.bitmap_width = bitmap_width; - this.bitmap_height = bitmap_height; - this.plane_width = plane_width; - this.plane_height = plane_height; - this.isForced = isForced; - } - - public long getStartDisplayTime() { return start_display_time; } - public Bitmap getBitmap() { return bitmap; } - public int getX() { return x; } - public int getY() { return y; } - public int getBitmapWidth() { return bitmap_width; } - public int getBitmapHeight() { return bitmap_height; } - public int getPlaneWidth() { return plane_width; } - public int getPlaneHeight() { return plane_height; } - public boolean isForcedSubtitle() { return isForced; } -} diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index ffe2ca31e0..0e63ee9ba4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -40,7 +40,6 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.text.ImageCue; import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -172,7 +171,6 @@ public final class SimpleExoPlayerView extends FrameLayout { private final View surfaceView; private final ImageView artworkView; private final SubtitleView subtitleView; - private final ImageView subtitleImageView; private final PlaybackControlView controller; private final ComponentListener componentListener; private final FrameLayout overlayFrameLayout; @@ -256,8 +254,6 @@ public final class SimpleExoPlayerView extends FrameLayout { subtitleView.setUserDefaultTextSize(); } - subtitleImageView = (ImageView) findViewById(R.id.exo_subtitles_image); - // Playback control view. View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); if (controllerPlaceholder != null) { @@ -619,70 +615,11 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public void onCues(List cues) { - boolean skipNormalCues = false; - if (subtitleImageView != null) { - - final ImageCue cue = (cues != null && !cues.isEmpty() && cues.get(0) instanceof ImageCue) ? (ImageCue) cues.get(0) : null; - skipNormalCues = (cue != null); - if (cue == null || (!subtitlesEnabled && !cue.isForcedSubtitle())) { - subtitleImageView.setImageBitmap(null); - subtitleImageView.setVisibility(View.INVISIBLE); - } - else { - handleImageCue(cue); - } - } - if (!skipNormalCues && subtitleView != null) { + if (subtitleView != null) { subtitleView.onCues(cues); } } - private void handleImageCue(ImageCue cue) { - Bitmap bitmap = cue.getBitmap(); - if (bitmap != null && surfaceView != null) { - int surfaceAnchorX = (int) surfaceView.getX(); - int surfaceAnchorY = (int) surfaceView.getY(); - int surfaceWidth = surfaceView.getWidth(); - int surfaceHeight = surfaceView.getHeight(); - int sourceWidth = cue.getPlaneWidth(); - int sourceHeight = cue.getPlaneHeight(); - int subAnchorX = cue.getX(); - int subAnchorY = cue.getY(); - int subScaleWidth = cue.getBitmapWidth(); - int subScaleHeight = cue.getBitmapHeight(); - - // they should change together as we keep the aspect ratio - if ((surfaceHeight != sourceHeight || surfaceWidth != sourceWidth) - && sourceHeight > 0 && sourceWidth > 0) { - double scale; - if (surfaceWidth != sourceWidth) - scale = (double) surfaceWidth / (double) sourceWidth; - else - scale = (double) surfaceHeight / (double) sourceHeight; - subScaleHeight = (int) (scale * subScaleHeight); - subScaleWidth = (int) (scale * subScaleWidth); - } - if (surfaceAnchorX != 0) - subAnchorX += surfaceAnchorX; - if (subAnchorY != 0) - subAnchorY += surfaceAnchorY; - - ViewGroup.LayoutParams params = subtitleImageView.getLayoutParams(); - params.width = subScaleWidth; - params.height = subScaleHeight; - subtitleImageView.setX(subAnchorX); - subtitleImageView.setY(subAnchorY); - subtitleImageView.setLayoutParams(params); - subtitleImageView.setImageBitmap(bitmap); - subtitleImageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); - subtitleImageView.setVisibility(View.VISIBLE); - } - else { - subtitleImageView.setImageBitmap(null); - subtitleImageView.setVisibility(View.INVISIBLE); - } - } - // SimpleExoPlayer.VideoListener implementation @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index 8c3ac77cb2..73c465fe95 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -18,11 +18,13 @@ package com.google.android.exoplayer2.ui; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Join; import android.graphics.Paint.Style; +import android.graphics.Rect; import android.graphics.RectF; import android.text.Layout.Alignment; import android.text.StaticLayout; @@ -63,6 +65,7 @@ import com.google.android.exoplayer2.util.Util; private final Paint paint; // Previous input variables. + private Bitmap cueBitmap; private CharSequence cueText; private Alignment cueTextAlignment; private float cueLine; @@ -74,6 +77,7 @@ import com.google.android.exoplayer2.util.Util; @Cue.AnchorType private int cuePositionAnchor; private float cueSize; + private float cueSizeHeight; private boolean applyEmbeddedStyles; private int foregroundColor; private int backgroundColor; @@ -93,6 +97,7 @@ import com.google.android.exoplayer2.util.Util; private int textLeft; private int textTop; private int textPaddingX; + private Rect bitmapRect; @SuppressWarnings("ResourceType") public SubtitlePainter(Context context) { @@ -142,40 +147,43 @@ import com.google.android.exoplayer2.util.Util; float bottomPaddingFraction, Canvas canvas, int cueBoxLeft, int cueBoxTop, int cueBoxRight, int cueBoxBottom) { CharSequence cueText = cue.text; - if (TextUtils.isEmpty(cueText)) { + boolean textIsEmpty = TextUtils.isEmpty(cueText); + if (textIsEmpty && cue.bitmap == null) { // Nothing to draw. return; } - if (!applyEmbeddedStyles) { + if (!applyEmbeddedStyles && !textIsEmpty) { // Strip out any embedded styling. cueText = cueText.toString(); } - if (areCharSequencesEqual(this.cueText, cueText) - && Util.areEqual(this.cueTextAlignment, cue.textAlignment) - && this.cueLine == cue.line - && this.cueLineType == cue.lineType - && Util.areEqual(this.cueLineAnchor, cue.lineAnchor) - && this.cuePosition == cue.position - && Util.areEqual(this.cuePositionAnchor, cue.positionAnchor) - && this.cueSize == cue.size - && this.applyEmbeddedStyles == applyEmbeddedStyles - && this.foregroundColor == style.foregroundColor - && this.backgroundColor == style.backgroundColor - && this.windowColor == style.windowColor - && this.edgeType == style.edgeType - && this.edgeColor == style.edgeColor - && Util.areEqual(this.textPaint.getTypeface(), style.typeface) - && this.textSizePx == textSizePx - && this.bottomPaddingFraction == bottomPaddingFraction - && this.parentLeft == cueBoxLeft - && this.parentTop == cueBoxTop - && this.parentRight == cueBoxRight - && this.parentBottom == cueBoxBottom) { + if (((cue.bitmap != null && cue.bitmap == cueBitmap) || + (!textIsEmpty && areCharSequencesEqual(this.cueText, cueText))) + && Util.areEqual(this.cueTextAlignment, cue.textAlignment) + && this.cueLine == cue.line + && this.cueLineType == cue.lineType + && Util.areEqual(this.cueLineAnchor, cue.lineAnchor) + && this.cuePosition == cue.position + && Util.areEqual(this.cuePositionAnchor, cue.positionAnchor) + && this.cueSize == cue.size + && this.applyEmbeddedStyles == applyEmbeddedStyles + && this.foregroundColor == style.foregroundColor + && this.backgroundColor == style.backgroundColor + && this.windowColor == style.windowColor + && this.edgeType == style.edgeType + && this.edgeColor == style.edgeColor + && Util.areEqual(this.textPaint.getTypeface(), style.typeface) + && this.textSizePx == textSizePx + && this.bottomPaddingFraction == bottomPaddingFraction + && this.parentLeft == cueBoxLeft + && this.parentTop == cueBoxTop + && this.parentRight == cueBoxRight + && this.parentBottom == cueBoxBottom) { // We can use the cached layout. drawLayout(canvas); return; } + this.cueBitmap = cue.bitmap; this.cueText = cueText; this.cueTextAlignment = cue.textAlignment; this.cueLine = cue.line; @@ -184,6 +192,7 @@ import com.google.android.exoplayer2.util.Util; this.cuePosition = cue.position; this.cuePositionAnchor = cue.positionAnchor; this.cueSize = cue.size; + this.cueSizeHeight = cue.size_height; this.applyEmbeddedStyles = applyEmbeddedStyles; this.foregroundColor = style.foregroundColor; this.backgroundColor = style.backgroundColor; @@ -198,6 +207,32 @@ import com.google.android.exoplayer2.util.Util; this.parentRight = cueBoxRight; this.parentBottom = cueBoxBottom; + if (this.cueBitmap != null) + setupBitmapLayout(); + else + setupTextLayout(); + + drawLayout(canvas); + } + + /** + * Setup {@link #textLayout} for later drawing. + */ + private void setupBitmapLayout() { + + int parentWidth = parentRight - parentLeft; + int parentHeight = parentBottom - parentTop; + int x = parentLeft + (int) ((float) parentWidth * cuePosition); + int y = parentTop + (int) ((float) parentHeight * cueLine); + bitmapRect = new Rect(x,y, + x + (int)((float) parentWidth * cueSize),y + (int)((float) parentHeight * cueSizeHeight)); + } + + /** + * Setup {@link #textLayout} for later drawing. + */ + private void setupTextLayout() { + int parentWidth = parentRight - parentLeft; int parentHeight = parentBottom - parentTop; @@ -275,8 +310,27 @@ import com.google.android.exoplayer2.util.Util; this.textLeft = textLeft; this.textTop = textTop; this.textPaddingX = textPaddingX; + } - drawLayout(canvas); + /** + * Draws {@link #textLayout} or {@link #cueBitmap} into the provided canvas. + * + * @param canvas The canvas into which to draw. + */ + private void drawLayout(Canvas canvas) { + if (cueBitmap != null) + drawBitmapLayout(canvas); + else + drawTextLayout(canvas); + } + + /** + * Draws {@link #cueBitmap} into the provided canvas. + * + * @param canvas The canvas into which to draw. + */ + private void drawBitmapLayout(Canvas canvas) { + canvas.drawBitmap(cueBitmap, null, bitmapRect, null); } /** @@ -284,7 +338,7 @@ import com.google.android.exoplayer2.util.Util; * * @param canvas The canvas into which to draw. */ - private void drawLayout(Canvas canvas) { + private void drawTextLayout(Canvas canvas) { final StaticLayout layout = textLayout; if (layout == null) { // Nothing to draw. diff --git a/library/src/main/res/layout/exo_simple_player_view.xml b/library/src/main/res/layout/exo_simple_player_view.xml index bfd5638713..1f59b7796d 100644 --- a/library/src/main/res/layout/exo_simple_player_view.xml +++ b/library/src/main/res/layout/exo_simple_player_view.xml @@ -36,12 +36,6 @@ android:layout_width="match_parent" android:layout_height="match_parent"/> - - Date: Wed, 21 Dec 2016 07:54:23 -0500 Subject: [PATCH 005/140] remove unneeded changes --- .../exoplayer2/ui/SimpleExoPlayerView.java | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 0e63ee9ba4..97c564a3a6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -178,7 +178,6 @@ public final class SimpleExoPlayerView extends FrameLayout { private SimpleExoPlayer player; private boolean useController; private boolean useArtwork; - private boolean subtitlesEnabled = false; private int controllerShowTimeoutMs; public SimpleExoPlayerView(Context context) { @@ -524,26 +523,14 @@ public final class SimpleExoPlayerView extends FrameLayout { return; } TrackSelectionArray selections = player.getCurrentTrackSelections(); - boolean quickExit = false; for (int i = 0; i < selections.length; i++) { - switch(player.getRendererType(i)) { - case C.TRACK_TYPE_VIDEO: - if (selections.get(i) != null) { - // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in - // onRenderedFirstFrame(). - - hideArtwork(); - quickExit = true; - } - break; - case C.TRACK_TYPE_TEXT: - if (selections.get(i) != null) - subtitlesEnabled = true; - break; + if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { + // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in + // onRenderedFirstFrame(). + hideArtwork(); + return; } } - if (quickExit) - return; // Video disabled so the shutter must be closed. if (shutterView != null) { shutterView.setVisibility(VISIBLE); @@ -614,7 +601,6 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public void onCues(List cues) { - if (subtitleView != null) { subtitleView.onCues(cues); } @@ -625,7 +611,6 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - if (contentFrame != null) { float aspectRatio = height == 0 ? 1 : (width * pixelWidthHeightRatio) / height; contentFrame.setAspectRatio(aspectRatio); From 0468a80d4125af669c0dadf66bd53144db323423 Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Thu, 5 Jan 2017 21:27:24 -0500 Subject: [PATCH 006/140] just use original plane width to calculate width proportion to use later as size and keep aspect ratio for height --- .../google/android/exoplayer2/text/Cue.java | 25 ++++--------------- .../exoplayer2/ui/SubtitlePainter.java | 7 +++--- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/src/main/java/com/google/android/exoplayer2/text/Cue.java index 23cce573f7..5acb29da33 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -90,8 +90,6 @@ public class Cue { *

* For horizontal text and {@link #lineType} equal to {@link #LINE_TYPE_FRACTION}, this is the * fractional vertical position relative to the top of the viewport. - *

- * If {@link #bitmap} is not null then this value is used to indicate the top position */ public final float line; /** @@ -126,8 +124,6 @@ public class Cue { * For horizontal text, this is the horizontal position relative to the left of the viewport. Note * that positioning is relative to the left of the viewport even in the case of right-to-left * text. - *

- * If {@link #bitmap} is not null then this value is used to indicate the left position */ public final float position; /** @@ -143,23 +139,15 @@ public class Cue { /** * The size of the cue box in the writing direction specified as a fraction of the viewport size * in that direction, or {@link #DIMEN_UNSET}. - *

- * If {@link #bitmap} is not null then this value is used to indicate the width */ public final float size; - /** - * The height size of the cue box when a {@link #bitmap} is set specified as a fraction of the - * viewport size in that direction, or {@link #DIMEN_UNSET}. - */ - public final float size_height; - /** * */ - public Cue(Bitmap bitmap, float left, float top, float size, float size_height) { - this(null, null, top, LINE_TYPE_FRACTION, TYPE_UNSET, left, TYPE_UNSET, size, size_height, - bitmap); + public Cue(Bitmap bitmap, float left, float top, int plane_width) { + this(null, null, top, LINE_TYPE_FRACTION, ANCHOR_TYPE_START, left, ANCHOR_TYPE_START, + (float) bitmap.getWidth() / plane_width, bitmap); } /** @@ -184,8 +172,7 @@ public class Cue { */ public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size) { - this(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size, - DIMEN_UNSET, null); + this(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size, null); } /** * @param text See {@link #text}. @@ -196,12 +183,11 @@ public class Cue { * @param position See {@link #position}. * @param positionAnchor See {@link #positionAnchor}. * @param size See {@link #size}. - * @param size_height See {@link #size_height}. * @param bitmap See {@link #bitmap}. */ private Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, - float size_height, Bitmap bitmap) { + Bitmap bitmap) { this.text = text; this.textAlignment = textAlignment; this.line = line; @@ -210,7 +196,6 @@ public class Cue { this.position = position; this.positionAnchor = positionAnchor; this.size = size; - this.size_height = size_height; this.bitmap = bitmap; } diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index 73c465fe95..b8b55e27ce 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -77,7 +77,6 @@ import com.google.android.exoplayer2.util.Util; @Cue.AnchorType private int cuePositionAnchor; private float cueSize; - private float cueSizeHeight; private boolean applyEmbeddedStyles; private int foregroundColor; private int backgroundColor; @@ -192,7 +191,6 @@ import com.google.android.exoplayer2.util.Util; this.cuePosition = cue.position; this.cuePositionAnchor = cue.positionAnchor; this.cueSize = cue.size; - this.cueSizeHeight = cue.size_height; this.applyEmbeddedStyles = applyEmbeddedStyles; this.foregroundColor = style.foregroundColor; this.backgroundColor = style.backgroundColor; @@ -224,8 +222,9 @@ import com.google.android.exoplayer2.util.Util; int parentHeight = parentBottom - parentTop; int x = parentLeft + (int) ((float) parentWidth * cuePosition); int y = parentTop + (int) ((float) parentHeight * cueLine); - bitmapRect = new Rect(x,y, - x + (int)((float) parentWidth * cueSize),y + (int)((float) parentHeight * cueSizeHeight)); + int width = (int) (parentWidth * cueSize); + int height = (int) (width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth())); + bitmapRect = new Rect(x, y, x + width, y + height); } /** From 933b7faadd97d57c0b1de625c2600aa0caa2347a Mon Sep 17 00:00:00 2001 From: twisstosin Date: Sun, 12 Feb 2017 23:06:51 +0100 Subject: [PATCH 007/140] Updated Gradle, Compile and Build Tools Version --- build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index b10a17de81..3f4bab597a 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.1' + classpath 'com.android.tools.build:gradle:2.2.3' classpath 'com.novoda:bintray-release:0.3.4' } } @@ -29,9 +29,9 @@ allprojects { jcenter() } project.ext { - compileSdkVersion=24 - targetSdkVersion=24 - buildToolsVersion='23.0.3' + compileSdkVersion=25 + targetSdkVersion=25 + buildToolsVersion='25' releaseRepoName = 'exoplayer' releaseUserOrg = 'google' releaseGroupId = 'com.google.android.exoplayer' From 460dc2561a07eca9e5bffaf334a0c9d6858581e9 Mon Sep 17 00:00:00 2001 From: twisstosin Date: Tue, 14 Feb 2017 19:23:08 +0100 Subject: [PATCH 008/140] Changed Text Size --- library/src/main/res/layout/exo_playback_control_view.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src/main/res/layout/exo_playback_control_view.xml b/library/src/main/res/layout/exo_playback_control_view.xml index f8ef5a6fdd..531ee4c6fa 100644 --- a/library/src/main/res/layout/exo_playback_control_view.xml +++ b/library/src/main/res/layout/exo_playback_control_view.xml @@ -58,7 +58,7 @@ Date: Mon, 30 Jan 2017 12:55:35 -0800 Subject: [PATCH 009/140] Fixed issue with TextRenderer and RawCC 608/708 captions in which all the captions prior to the playback position would be rendered in [] succession when a VOD stream starts. Instead of clearing the DECODE_ONLY flag for all input buffers in TextRenderer (i.e. for all caption types), we now only clear it on the output buffer in SimpleSubtitleDecoder. The number if input buffers in CeaDecoder has also been increased to reduce the delay before captions appear for playback sessions that start far ahead into the content. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146028680 --- .../android/exoplayer2/text/SimpleSubtitleDecoder.java | 3 +++ .../com/google/android/exoplayer2/text/TextRenderer.java | 2 -- .../google/android/exoplayer2/text/cea/CeaDecoder.java | 8 +++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java b/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java index ae3bd309ff..dd25ef8345 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.SimpleDecoder; import java.nio.ByteBuffer; @@ -68,6 +69,8 @@ public abstract class SimpleSubtitleDecoder extends ByteBuffer inputData = inputBuffer.data; Subtitle subtitle = decode(inputData.array(), inputData.limit()); outputBuffer.setContent(inputBuffer.timeUs, subtitle, inputBuffer.subsampleOffsetUs); + // Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]). + outputBuffer.clearFlag(C.BUFFER_FLAG_DECODE_ONLY); return null; } catch (SubtitleDecoderException e) { return e; diff --git a/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index 8dbde1be5e..649575865e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -191,8 +191,6 @@ public final class TextRenderer extends BaseRenderer implements Callback { // Try and read the next subtitle from the source. int result = readSource(formatHolder, nextInputBuffer); if (result == C.RESULT_BUFFER_READ) { - // Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]) and queue the buffer. - nextInputBuffer.clearFlag(C.BUFFER_FLAG_DECODE_ONLY); if (nextInputBuffer.isEndOfStream()) { inputStreamEnded = true; } else { diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java index f479050d57..fac0982e65 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java @@ -75,7 +75,13 @@ import java.util.TreeSet; public void queueInputBuffer(SubtitleInputBuffer inputBuffer) throws SubtitleDecoderException { Assertions.checkArgument(inputBuffer != null); Assertions.checkArgument(inputBuffer == dequeuedInputBuffer); - queuedInputBuffers.add(inputBuffer); + if (inputBuffer.isDecodeOnly()) { + // We can drop this buffer early (i.e. before it would be decoded) as the CEA formats allow + // for decoding to begin mid-stream. + releaseInputBuffer(inputBuffer); + } else { + queuedInputBuffers.add(inputBuffer); + } dequeuedInputBuffer = null; } From 8f482cb2ed99b49826b2140aaa53b270422560ed Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 31 Jan 2017 07:45:03 -0800 Subject: [PATCH 010/140] Fixing some Javadoc errors - Cea708 Javadoc references private internals with @link. Doc about device accessibility settings doesn't relate to the decoder, either. - Creator Javadoc unnecessary. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146118092 --- .../main/java/com/google/android/exoplayer2/Format.java | 3 --- .../google/android/exoplayer2/text/cea/Cea708Decoder.java | 7 ------- 2 files changed, 10 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/Format.java b/library/src/main/java/com/google/android/exoplayer2/Format.java index 0b558153fd..bf113119a6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/src/main/java/com/google/android/exoplayer2/Format.java @@ -672,9 +672,6 @@ public final class Format implements Parcelable { dest.writeParcelable(metadata, 0); } - /** - * {@link Creator} implementation. - */ public static final Creator CREATOR = new Creator() { @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java index 5ca5ce1270..fe97dc62a5 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -43,13 +43,6 @@ import java.util.List; /** * A {@link SubtitleDecoder} for CEA-708 (also known as "EIA-708"). - * - *

This implementation does not provide full compatibility with the CEA-708 specification. Note - * that only the default pen/text and window/cue colors (i.e. text with - * {@link CueBuilder#COLOR_SOLID_WHITE} foreground and {@link CueBuilder#COLOR_SOLID_BLACK} - * background, and cues with {@link CueBuilder#COLOR_SOLID_BLACK} fill) will be overridden with - * device accessibility settings; all others will use the colors and opacity specified by the - * caption data. */ public final class Cea708Decoder extends CeaDecoder { From af98ca661a43c3ef4ee52e57e5d592549e406eb6 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 31 Jan 2017 07:47:28 -0800 Subject: [PATCH 011/140] Refactor DashTest class Moved DashHostedTest to top level classes. Added DashHostedTest.Builder. Move widevine offline tests to separate class with custom setUp and tearDown methods. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146118310 --- .../playbacktests/gts/DashHostedTest.java | 462 +++++++ .../playbacktests/gts/DashTest.java | 1098 +++++------------ .../playbacktests/gts/DashTestData.java | 141 +++ .../gts/DashWidevineOfflineTest.java | 180 +++ .../playbacktests/util/ExoHostedTest.java | 3 +- 5 files changed, 1072 insertions(+), 812 deletions(-) create mode 100644 playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java create mode 100644 playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java create mode 100644 playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java new file mode 100644 index 0000000000..24765f282d --- /dev/null +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java @@ -0,0 +1,462 @@ +/* + * Copyright (C) 2017 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.playbacktests.gts; + +import static com.google.android.exoplayer2.C.WIDEVINE_UUID; + +import android.annotation.TargetApi; +import android.app.Instrumentation; +import android.media.MediaDrm; +import android.media.UnsupportedSchemeException; +import android.net.Uri; +import android.util.Log; +import android.view.Surface; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.DefaultLoadControl; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; +import com.google.android.exoplayer2.drm.MediaDrmCallback; +import com.google.android.exoplayer2.drm.UnsupportedDrmException; +import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; +import com.google.android.exoplayer2.playbacktests.util.ActionSchedule; +import com.google.android.exoplayer2.playbacktests.util.DebugSimpleExoPlayer; +import com.google.android.exoplayer2.playbacktests.util.DecoderCountersUtil; +import com.google.android.exoplayer2.playbacktests.util.ExoHostedTest; +import com.google.android.exoplayer2.playbacktests.util.HostActivity; +import com.google.android.exoplayer2.playbacktests.util.HostActivity.HostedTest; +import com.google.android.exoplayer2.playbacktests.util.MetricsLogger; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; +import com.google.android.exoplayer2.trackselection.FixedTrackSelection; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.RandomTrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import junit.framework.AssertionFailedError; + +/** + * A {@link HostedTest} for DASH playback tests. + */ +@TargetApi(16) +public final class DashHostedTest extends ExoHostedTest { + + /** {@link DashHostedTest} builder. */ + public static final class Builder { + + private static final long TEST_TIMEOUT_MS = 5 * 60 * 1000; + + private static final String REPORT_NAME = "GtsExoPlayerTestCases"; + private static final String REPORT_OBJECT_NAME = "playbacktest"; + + // Whether adaptive tests should enable video formats beyond those mandated by the Android CDD + // if the device advertises support for them. + private static final boolean ALLOW_ADDITIONAL_VIDEO_FORMATS = Util.SDK_INT >= 24; + + private final String tag; + + private String streamName; + private boolean fullPlaybackNoSeeking; + private String audioFormat; + private boolean canIncludeAdditionalVideoFormats; + private ActionSchedule actionSchedule; + private byte[] offlineLicenseKeySetId; + private String[] videoFormats; + private String manifestUrl; + private boolean useL1Widevine; + private String widevineLicenseUrl; + + public Builder(String tag) { + this.tag = tag; + } + + public Builder setStreamName(String streamName) { + this.streamName = streamName; + return this; + } + + public Builder setFullPlaybackNoSeeking(boolean fullPlaybackNoSeeking) { + this.fullPlaybackNoSeeking = fullPlaybackNoSeeking; + return this; + } + + public Builder setCanIncludeAdditionalVideoFormats( + boolean canIncludeAdditionalVideoFormats) { + this.canIncludeAdditionalVideoFormats = canIncludeAdditionalVideoFormats + && ALLOW_ADDITIONAL_VIDEO_FORMATS; + return this; + } + + public Builder setActionSchedule(ActionSchedule actionSchedule) { + this.actionSchedule = actionSchedule; + return this; + } + + public Builder setOfflineLicenseKeySetId(byte[] offlineLicenseKeySetId) { + this.offlineLicenseKeySetId = offlineLicenseKeySetId; + return this; + } + + public Builder setAudioVideoFormats(String audioFormat, String... videoFormats) { + this.audioFormat = audioFormat; + this.videoFormats = videoFormats; + return this; + } + + public Builder setManifestUrl(String manifestUrl) { + this.manifestUrl = MANIFEST_URL_PREFIX + manifestUrl; + return this; + } + + public Builder setManifestUrlForWidevine(String manifestUrl, String videoMimeType) { + this.useL1Widevine = isL1WidevineAvailable(videoMimeType); + this.manifestUrl = getWidevineManifestUrl(manifestUrl, useL1Widevine); + this.widevineLicenseUrl = getWidevineLicenseUrl(useL1Widevine); + return this; + } + + private DashHostedTest createDashHostedTest(boolean canIncludeAdditionalVideoFormats, + boolean isCddLimitedRetry, Instrumentation instrumentation) { + MetricsLogger metricsLogger = MetricsLogger.Factory.createDefault(instrumentation, tag, + REPORT_NAME, REPORT_OBJECT_NAME); + return new DashHostedTest(tag, streamName, manifestUrl, metricsLogger, fullPlaybackNoSeeking, + audioFormat, canIncludeAdditionalVideoFormats, isCddLimitedRetry, actionSchedule, + offlineLicenseKeySetId, widevineLicenseUrl, useL1Widevine, videoFormats); + } + + public void runTest(HostActivity activity, Instrumentation instrumentation) { + DashHostedTest test = createDashHostedTest(canIncludeAdditionalVideoFormats, false, + instrumentation); + activity.runTest(test, TEST_TIMEOUT_MS); + // Retry test exactly once if adaptive test fails due to excessive dropped buffers when + // playing non-CDD required formats (b/28220076). + if (test.needsCddLimitedRetry) { + activity.runTest(createDashHostedTest(false, true, instrumentation), TEST_TIMEOUT_MS); + } + } + + } + + private static final String AUDIO_TAG_SUFFIX = ":Audio"; + private static final String VIDEO_TAG_SUFFIX = ":Video"; + static final int VIDEO_RENDERER_INDEX = 0; + static final int AUDIO_RENDERER_INDEX = 1; + + private static final int MIN_LOADABLE_RETRY_COUNT = 10; + private static final int MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES = 10; + private static final float MAX_DROPPED_VIDEO_FRAME_FRACTION = 0.01f; + + private static final String MANIFEST_URL_PREFIX = "https://storage.googleapis.com/exoplayer-test-" + + "media-1/gen-3/screens/dash-vod-single-segment/"; + + private static final String WIDEVINE_L1_SUFFIX = "-hw.mpd"; + private static final String WIDEVINE_L3_SUFFIX = "-sw.mpd"; + + private static final String WIDEVINE_LICENSE_URL = + "https://proxy.uat.widevine.com/proxy?provider=widevine_test&video_id="; + private static final String WIDEVINE_SW_CRYPTO_CONTENT_ID = "exoplayer_test_1"; + private static final String WIDEVINE_HW_SECURE_DECODE_CONTENT_ID = "exoplayer_test_2"; + private static final String WIDEVINE_SECURITY_LEVEL_1 = "L1"; + private static final String WIDEVINE_SECURITY_LEVEL_3 = "L3"; + private static final String SECURITY_LEVEL_PROPERTY = "securityLevel"; + + private final String streamName; + private final String manifestUrl; + private final MetricsLogger metricsLogger; + private final boolean fullPlaybackNoSeeking; + private final boolean isCddLimitedRetry; + private final DashTestTrackSelector trackSelector; + private final byte[] offlineLicenseKeySetId; + private final String widevineLicenseUrl; + private final boolean useL1Widevine; + + boolean needsCddLimitedRetry; + + public static String getWidevineManifestUrl(String manifestUrl, boolean useL1Widevine) { + return MANIFEST_URL_PREFIX + manifestUrl + + (useL1Widevine ? WIDEVINE_L1_SUFFIX : WIDEVINE_L3_SUFFIX); + } + + public static String getWidevineLicenseUrl(boolean useL1Widevine) { + return WIDEVINE_LICENSE_URL + + (useL1Widevine ? WIDEVINE_HW_SECURE_DECODE_CONTENT_ID : WIDEVINE_SW_CRYPTO_CONTENT_ID); + } + + @TargetApi(18) + @SuppressWarnings("ResourceType") + public static boolean isL1WidevineAvailable(String videoMimeType) { + try { + // Force L3 if secure decoder is not available. + if (MediaCodecUtil.getDecoderInfo(videoMimeType, true) == null) { + return false; + } + + MediaDrm mediaDrm = new MediaDrm(WIDEVINE_UUID); + String securityProperty = mediaDrm.getPropertyString(SECURITY_LEVEL_PROPERTY); + mediaDrm.release(); + return WIDEVINE_SECURITY_LEVEL_1.equals(securityProperty); + } catch (MediaCodecUtil.DecoderQueryException | UnsupportedSchemeException e) { + throw new IllegalStateException(e); + } + } + + /** + * @param tag A tag to use for logging. + * @param streamName The name of the test stream for metric logging. + * @param manifestUrl The manifest url. + * @param metricsLogger Logger to log metrics from the test. + * @param fullPlaybackNoSeeking Whether the test will play the entire source with no seeking. + * @param audioFormat The audio format. + * @param canIncludeAdditionalVideoFormats Whether to use video formats in addition to those + * listed in the videoFormats argument, if the device is capable of playing them. + * @param isCddLimitedRetry Whether this is a CDD limited retry following a previous failure. + * @param actionSchedule The action schedule for the test. + * @param offlineLicenseKeySetId The key set id of the license to be used. + * @param widevineLicenseUrl If the video is Widevine encrypted, this is the license url + * otherwise null. + * @param useL1Widevine Whether to use L1 Widevine. + * @param videoFormats The video formats. + */ + private DashHostedTest(String tag, String streamName, String manifestUrl, + MetricsLogger metricsLogger, boolean fullPlaybackNoSeeking, String audioFormat, + boolean canIncludeAdditionalVideoFormats, boolean isCddLimitedRetry, + ActionSchedule actionSchedule, byte[] offlineLicenseKeySetId, String widevineLicenseUrl, + boolean useL1Widevine, String... videoFormats) { + super(tag, fullPlaybackNoSeeking); + Assertions.checkArgument(!(isCddLimitedRetry && canIncludeAdditionalVideoFormats)); + this.streamName = streamName; + this.manifestUrl = manifestUrl; + this.metricsLogger = metricsLogger; + this.fullPlaybackNoSeeking = fullPlaybackNoSeeking; + this.isCddLimitedRetry = isCddLimitedRetry; + this.offlineLicenseKeySetId = offlineLicenseKeySetId; + this.widevineLicenseUrl = widevineLicenseUrl; + this.useL1Widevine = useL1Widevine; + trackSelector = new DashTestTrackSelector(tag, audioFormat, videoFormats, + canIncludeAdditionalVideoFormats); + if (actionSchedule != null) { + setSchedule(actionSchedule); + } + } + + @Override + protected MappingTrackSelector buildTrackSelector(HostActivity host, + BandwidthMeter bandwidthMeter) { + return trackSelector; + } + + @Override + protected DefaultDrmSessionManager buildDrmSessionManager( + final String userAgent) { + if (widevineLicenseUrl == null) { + return null; + } + try { + MediaDrmCallback drmCallback = new HttpMediaDrmCallback(widevineLicenseUrl, + new DefaultHttpDataSourceFactory(userAgent)); + DefaultDrmSessionManager drmSessionManager = + DefaultDrmSessionManager.newWidevineInstance(drmCallback, null, null, null); + if (!useL1Widevine) { + drmSessionManager.setPropertyString( + SECURITY_LEVEL_PROPERTY, WIDEVINE_SECURITY_LEVEL_3); + } + if (offlineLicenseKeySetId != null) { + drmSessionManager.setMode(DefaultDrmSessionManager.MODE_PLAYBACK, + offlineLicenseKeySetId); + } + return drmSessionManager; + } catch (UnsupportedDrmException e) { + throw new IllegalStateException(e); + } + } + + @Override + protected SimpleExoPlayer buildExoPlayer(HostActivity host, Surface surface, + MappingTrackSelector trackSelector, + DrmSessionManager drmSessionManager) { + SimpleExoPlayer player = new DebugSimpleExoPlayer(host, trackSelector, + new DefaultLoadControl(), drmSessionManager); + player.setVideoSurface(surface); + return player; + } + + @Override + protected MediaSource buildSource(HostActivity host, String userAgent, + TransferListener mediaTransferListener) { + DataSource.Factory manifestDataSourceFactory = new DefaultDataSourceFactory(host, userAgent); + DataSource.Factory mediaDataSourceFactory = new DefaultDataSourceFactory(host, userAgent, + mediaTransferListener); + Uri manifestUri = Uri.parse(manifestUrl); + DefaultDashChunkSource.Factory chunkSourceFactory = new DefaultDashChunkSource.Factory( + mediaDataSourceFactory); + return new DashMediaSource(manifestUri, manifestDataSourceFactory, chunkSourceFactory, + MIN_LOADABLE_RETRY_COUNT, 0 /* livePresentationDelayMs */, null, null); + } + + @Override + protected void logMetrics(DecoderCounters audioCounters, DecoderCounters videoCounters) { + metricsLogger.logMetric(MetricsLogger.KEY_TEST_NAME, streamName); + metricsLogger.logMetric(MetricsLogger.KEY_IS_CDD_LIMITED_RETRY, isCddLimitedRetry); + metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_DROPPED_COUNT, + videoCounters.droppedOutputBufferCount); + metricsLogger.logMetric(MetricsLogger.KEY_MAX_CONSECUTIVE_FRAMES_DROPPED_COUNT, + videoCounters.maxConsecutiveDroppedOutputBufferCount); + metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_SKIPPED_COUNT, + videoCounters.skippedOutputBufferCount); + metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_RENDERED_COUNT, + videoCounters.renderedOutputBufferCount); + metricsLogger.close(); + } + + @Override + protected void assertPassed(DecoderCounters audioCounters, DecoderCounters videoCounters) { + if (fullPlaybackNoSeeking) { + // We shouldn't have skipped any output buffers. + DecoderCountersUtil.assertSkippedOutputBufferCount(tag + AUDIO_TAG_SUFFIX, audioCounters, 0); + DecoderCountersUtil.assertSkippedOutputBufferCount(tag + VIDEO_TAG_SUFFIX, videoCounters, 0); + // We allow one fewer output buffer due to the way that MediaCodecRenderer and the + // underlying decoders handle the end of stream. This should be tightened up in the future. + DecoderCountersUtil.assertTotalOutputBufferCount(tag + AUDIO_TAG_SUFFIX, audioCounters, + audioCounters.inputBufferCount - 1, audioCounters.inputBufferCount); + DecoderCountersUtil.assertTotalOutputBufferCount(tag + VIDEO_TAG_SUFFIX, videoCounters, + videoCounters.inputBufferCount - 1, videoCounters.inputBufferCount); + } + try { + int droppedFrameLimit = (int) Math.ceil(MAX_DROPPED_VIDEO_FRAME_FRACTION + * DecoderCountersUtil.getTotalOutputBuffers(videoCounters)); + // Assert that performance is acceptable. + // Assert that total dropped frames were within limit. + DecoderCountersUtil.assertDroppedOutputBufferLimit(tag + VIDEO_TAG_SUFFIX, videoCounters, + droppedFrameLimit); + // Assert that consecutive dropped frames were within limit. + DecoderCountersUtil.assertConsecutiveDroppedOutputBufferLimit(tag + VIDEO_TAG_SUFFIX, + videoCounters, MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES); + } catch (AssertionFailedError e) { + if (trackSelector.includedAdditionalVideoFormats) { + // Retry limiting to CDD mandated formats (b/28220076). + Log.e(tag, "Too many dropped or consecutive dropped frames.", e); + needsCddLimitedRetry = true; + } else { + throw e; + } + } + } + + private static final class DashTestTrackSelector extends MappingTrackSelector { + + private final String tag; + private final String audioFormatId; + private final String[] videoFormatIds; + private final boolean canIncludeAdditionalVideoFormats; + + public boolean includedAdditionalVideoFormats; + + private DashTestTrackSelector(String tag, String audioFormatId, String[] videoFormatIds, + boolean canIncludeAdditionalVideoFormats) { + this.tag = tag; + this.audioFormatId = audioFormatId; + this.videoFormatIds = videoFormatIds; + this.canIncludeAdditionalVideoFormats = canIncludeAdditionalVideoFormats; + } + + @Override + protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, + TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) + throws ExoPlaybackException { + Assertions.checkState(rendererCapabilities[VIDEO_RENDERER_INDEX].getTrackType() + == C.TRACK_TYPE_VIDEO); + Assertions.checkState(rendererCapabilities[AUDIO_RENDERER_INDEX].getTrackType() + == C.TRACK_TYPE_AUDIO); + Assertions.checkState(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].length == 1); + Assertions.checkState(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].length == 1); + TrackSelection[] selections = new TrackSelection[rendererCapabilities.length]; + selections[VIDEO_RENDERER_INDEX] = new RandomTrackSelection( + rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0), + getVideoTrackIndices(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0), + rendererFormatSupports[VIDEO_RENDERER_INDEX][0], videoFormatIds, + canIncludeAdditionalVideoFormats), + 0 /* seed */); + selections[AUDIO_RENDERER_INDEX] = new FixedTrackSelection( + rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0), + getTrackIndex(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0), audioFormatId)); + includedAdditionalVideoFormats = + selections[VIDEO_RENDERER_INDEX].length() > videoFormatIds.length; + return selections; + } + + private int[] getVideoTrackIndices(TrackGroup trackGroup, int[] formatSupport, + String[] formatIds, boolean canIncludeAdditionalFormats) { + List trackIndices = new ArrayList<>(); + + // Always select explicitly listed representations. + for (String formatId : formatIds) { + int trackIndex = getTrackIndex(trackGroup, formatId); + Log.d(tag, "Adding base video format: " + + Format.toLogString(trackGroup.getFormat(trackIndex))); + trackIndices.add(trackIndex); + } + + // Select additional video representations, if supported by the device. + if (canIncludeAdditionalFormats) { + for (int i = 0; i < trackGroup.length; i++) { + if (!trackIndices.contains(i) && isFormatHandled(formatSupport[i])) { + Log.d(tag, "Adding extra video format: " + + Format.toLogString(trackGroup.getFormat(i))); + trackIndices.add(i); + } + } + } + + int[] trackIndicesArray = Util.toArray(trackIndices); + Arrays.sort(trackIndicesArray); + return trackIndicesArray; + } + + private static int getTrackIndex(TrackGroup trackGroup, String formatId) { + for (int i = 0; i < trackGroup.length; i++) { + if (trackGroup.getFormat(i).id.equals(formatId)) { + return i; + } + } + throw new IllegalStateException("Format " + formatId + " not found."); + } + + private static boolean isFormatHandled(int formatSupport) { + return (formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK) + == RendererCapabilities.FORMAT_HANDLED; + } + + } + +} diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java index 5752058c4e..6ae66f24e1 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java @@ -15,63 +15,15 @@ */ package com.google.android.exoplayer2.playbacktests.gts; -import android.annotation.TargetApi; -import android.media.MediaDrm; -import android.media.MediaDrm.MediaDrmStateException; -import android.media.UnsupportedSchemeException; -import android.net.Uri; import android.test.ActivityInstrumentationTestCase2; -import android.util.Log; -import android.util.Pair; -import android.view.Surface; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultLoadControl; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.RendererCapabilities; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.decoder.DecoderCounters; -import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; -import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; -import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; -import com.google.android.exoplayer2.drm.MediaDrmCallback; -import com.google.android.exoplayer2.drm.OfflineLicenseHelper; -import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.playbacktests.util.ActionSchedule; -import com.google.android.exoplayer2.playbacktests.util.DebugSimpleExoPlayer; -import com.google.android.exoplayer2.playbacktests.util.DecoderCountersUtil; -import com.google.android.exoplayer2.playbacktests.util.ExoHostedTest; import com.google.android.exoplayer2.playbacktests.util.HostActivity; -import com.google.android.exoplayer2.playbacktests.util.MetricsLogger; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; -import com.google.android.exoplayer2.trackselection.FixedTrackSelection; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector; -import com.google.android.exoplayer2.trackselection.RandomTrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.upstream.BandwidthMeter; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.exoplayer2.upstream.TransferListener; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; -import junit.framework.AssertionFailedError; /** * Tests DASH playbacks using {@link ExoPlayer}. @@ -79,147 +31,6 @@ import junit.framework.AssertionFailedError; public final class DashTest extends ActivityInstrumentationTestCase2 { private static final String TAG = "DashTest"; - private static final String VIDEO_TAG = TAG + ":Video"; - private static final String AUDIO_TAG = TAG + ":Audio"; - private static final String REPORT_NAME = "GtsExoPlayerTestCases"; - private static final String REPORT_OBJECT_NAME = "playbacktest"; - private static final int VIDEO_RENDERER_INDEX = 0; - private static final int AUDIO_RENDERER_INDEX = 1; - - private static final long TEST_TIMEOUT_MS = 5 * 60 * 1000; - private static final int MIN_LOADABLE_RETRY_COUNT = 10; - private static final int MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES = 10; - private static final float MAX_DROPPED_VIDEO_FRAME_FRACTION = 0.01f; - - private static final String MANIFEST_URL_PREFIX = "https://storage.googleapis.com/exoplayer-test-" - + "media-1/gen-3/screens/dash-vod-single-segment/"; - // Clear content manifests. - private static final String H264_MANIFEST = "manifest-h264.mpd"; - private static final String H265_MANIFEST = "manifest-h265.mpd"; - private static final String VP9_MANIFEST = "manifest-vp9.mpd"; - private static final String H264_23_MANIFEST = "manifest-h264-23.mpd"; - private static final String H264_24_MANIFEST = "manifest-h264-24.mpd"; - private static final String H264_29_MANIFEST = "manifest-h264-29.mpd"; - // Widevine encrypted content manifests. - private static final String WIDEVINE_H264_MANIFEST_PREFIX = "manifest-h264-enc"; - private static final String WIDEVINE_H265_MANIFEST_PREFIX = "manifest-h265-enc"; - private static final String WIDEVINE_VP9_MANIFEST_PREFIX = "manifest-vp9-enc"; - private static final String WIDEVINE_H264_23_MANIFEST_PREFIX = "manifest-h264-23-enc"; - private static final String WIDEVINE_H264_24_MANIFEST_PREFIX = "manifest-h264-24-enc"; - private static final String WIDEVINE_H264_29_MANIFEST_PREFIX = "manifest-h264-29-enc"; - private static final String WIDEVINE_L1_SUFFIX = "-hw.mpd"; - private static final String WIDEVINE_L3_SUFFIX = "-sw.mpd"; - - private static final String AAC_AUDIO_REPRESENTATION_ID = "141"; - private static final String H264_BASELINE_240P_VIDEO_REPRESENTATION_ID = "avc-baseline-240"; - private static final String H264_BASELINE_480P_VIDEO_REPRESENTATION_ID = "avc-baseline-480"; - private static final String H264_MAIN_240P_VIDEO_REPRESENTATION_ID = "avc-main-240"; - private static final String H264_MAIN_480P_VIDEO_REPRESENTATION_ID = "avc-main-480"; - // The highest quality H264 format mandated by the Android CDD. - private static final String H264_CDD_FIXED = Util.SDK_INT < 23 - ? H264_BASELINE_480P_VIDEO_REPRESENTATION_ID : H264_MAIN_480P_VIDEO_REPRESENTATION_ID; - // Multiple H264 formats mandated by the Android CDD. Note: The CDD actually mandated main profile - // support from API level 23, but we opt to test only from 24 due to known issues on API level 23 - // when switching between baseline and main profiles on certain devices. - private static final String[] H264_CDD_ADAPTIVE = Util.SDK_INT < 24 - ? new String[] { - H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, - H264_BASELINE_480P_VIDEO_REPRESENTATION_ID} - : new String[] { - H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, - H264_BASELINE_480P_VIDEO_REPRESENTATION_ID, - H264_MAIN_240P_VIDEO_REPRESENTATION_ID, - H264_MAIN_480P_VIDEO_REPRESENTATION_ID}; - - private static final String H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID = - "avc-baseline-480-23"; - private static final String H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID = - "avc-baseline-480-24"; - private static final String H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID = - "avc-baseline-480-29"; - - private static final String H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = "hevc-main-288"; - private static final String H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = "hevc-main-360"; - // The highest quality H265 format mandated by the Android CDD. - private static final String H265_CDD_FIXED = H265_BASELINE_360P_VIDEO_REPRESENTATION_ID; - // Multiple H265 formats mandated by the Android CDD. - private static final String[] H265_CDD_ADAPTIVE = - new String[] { - H265_BASELINE_288P_VIDEO_REPRESENTATION_ID, - H265_BASELINE_360P_VIDEO_REPRESENTATION_ID}; - - private static final String VORBIS_AUDIO_REPRESENTATION_ID = "4"; - private static final String VP9_180P_VIDEO_REPRESENTATION_ID = "0"; - private static final String VP9_360P_VIDEO_REPRESENTATION_ID = "1"; - // The highest quality VP9 format mandated by the Android CDD. - private static final String VP9_CDD_FIXED = VP9_360P_VIDEO_REPRESENTATION_ID; - // Multiple VP9 formats mandated by the Android CDD. - private static final String[] VP9_CDD_ADAPTIVE = - new String[] { - VP9_180P_VIDEO_REPRESENTATION_ID, - VP9_360P_VIDEO_REPRESENTATION_ID}; - - // Widevine encrypted content representation ids. - private static final String WIDEVINE_AAC_AUDIO_REPRESENTATION_ID = "0"; - private static final String WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID = "1"; - private static final String WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID = "2"; - private static final String WIDEVINE_H264_MAIN_240P_VIDEO_REPRESENTATION_ID = "3"; - private static final String WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID = "4"; - // The highest quality H264 format mandated by the Android CDD. - private static final String WIDEVINE_H264_CDD_FIXED = Util.SDK_INT < 23 - ? WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID - : WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID; - // Multiple H264 formats mandated by the Android CDD. Note: The CDD actually mandated main profile - // support from API level 23, but we opt to test only from 24 due to known issues on API level 23 - // when switching between baseline and main profiles on certain devices. - private static final String[] WIDEVINE_H264_CDD_ADAPTIVE = Util.SDK_INT < 24 - ? new String[] { - WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, - WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID} - : new String[] { - WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, - WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID, - WIDEVINE_H264_MAIN_240P_VIDEO_REPRESENTATION_ID, - WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID}; - - private static final String WIDEVINE_H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID = "2"; - private static final String WIDEVINE_H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID = "2"; - private static final String WIDEVINE_H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID = "2"; - - private static final String WIDEVINE_H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = "1"; - private static final String WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = "2"; - // The highest quality H265 format mandated by the Android CDD. - private static final String WIDEVINE_H265_CDD_FIXED = - WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID; - // Multiple H265 formats mandated by the Android CDD. - private static final String[] WIDEVINE_H265_CDD_ADAPTIVE = - new String[] { - WIDEVINE_H265_BASELINE_288P_VIDEO_REPRESENTATION_ID, - WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID}; - - private static final String WIDEVINE_VORBIS_AUDIO_REPRESENTATION_ID = "0"; - private static final String WIDEVINE_VP9_180P_VIDEO_REPRESENTATION_ID = "1"; - private static final String WIDEVINE_VP9_360P_VIDEO_REPRESENTATION_ID = "2"; - // The highest quality VP9 format mandated by the Android CDD. - private static final String WIDEVINE_VP9_CDD_FIXED = VP9_360P_VIDEO_REPRESENTATION_ID; - // Multiple VP9 formats mandated by the Android CDD. - private static final String[] WIDEVINE_VP9_CDD_ADAPTIVE = - new String[] { - WIDEVINE_VP9_180P_VIDEO_REPRESENTATION_ID, - WIDEVINE_VP9_360P_VIDEO_REPRESENTATION_ID}; - - private static final String WIDEVINE_LICENSE_URL = - "https://proxy.uat.widevine.com/proxy?provider=widevine_test&video_id="; - private static final String WIDEVINE_SW_CRYPTO_CONTENT_ID = "exoplayer_test_1"; - private static final String WIDEVINE_HW_SECURE_DECODE_CONTENT_ID = "exoplayer_test_2"; - private static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL); - private static final String WIDEVINE_SECURITY_LEVEL_1 = "L1"; - private static final String WIDEVINE_SECURITY_LEVEL_3 = "L3"; - private static final String SECURITY_LEVEL_PROPERTY = "securityLevel"; - - // Whether adaptive tests should enable video formats beyond those mandated by the Android CDD - // if the device advertises support for them. - private static final boolean ALLOW_ADDITIONAL_VIDEO_FORMATS = Util.SDK_INT >= 24; private static final ActionSchedule SEEKING_SCHEDULE = new ActionSchedule.Builder(TAG) .delay(10000).seek(15000) @@ -229,33 +40,33 @@ public final class DashTest extends ActivityInstrumentationTestCase2 0) { - synchronized (this) { - wait(licenseDuration * 1000 + 2000); - } - long previousDuration = licenseDuration; - licenseDuration = helper.getLicenseDurationRemainingSec().first; - assertTrue("License duration should be decreasing.", previousDuration > licenseDuration); - } - - // DefaultDrmSessionManager should renew the license and stream play fine - testDashPlayback(getActivity(), streamName, null, true, parameters, - WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, false, keySetId, WIDEVINE_H264_CDD_FIXED); - } finally { - helper.releaseResources(); - } - } - - public void testWidevineOfflineLicenseExpiresOnPause() throws Exception { - if (Util.SDK_INT < 22) { - // Pass. - return; - } - String streamName = "test_widevine_h264_fixed_offline"; - DashHostedTestEncParameters parameters = newDashHostedTestEncParameters( - WIDEVINE_H264_MANIFEST_PREFIX, true, MimeTypes.VIDEO_H264); - TestOfflineLicenseHelper helper = new TestOfflineLicenseHelper(parameters); - try { - byte[] keySetId = helper.downloadLicense(); - // During playback pause until the license expires then continue playback - Pair licenseDurationRemainingSec = helper.getLicenseDurationRemainingSec(); - long licenseDuration = licenseDurationRemainingSec.first; - assertTrue("License duration should be less than 30 sec. " - + "Server settings might have changed.", licenseDuration < 30); - ActionSchedule schedule = new ActionSchedule.Builder(TAG) - .delay(3000).pause().delay(licenseDuration * 1000 + 2000).play().build(); - // DefaultDrmSessionManager should renew the license and stream play fine - testDashPlayback(getActivity(), streamName, schedule, true, parameters, - WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, false, keySetId, WIDEVINE_H264_CDD_FIXED); - } finally { - helper.releaseResources(); - } + new DashHostedTest.Builder(TAG) + .setStreamName("test_widevine_29fps_h264_fixed") + .setManifestUrlForWidevine(DashTestData.WIDEVINE_H264_29_MANIFEST_PREFIX, + MimeTypes.VIDEO_H264) + .setFullPlaybackNoSeeking(true) + .setCanIncludeAdditionalVideoFormats(false) + .setAudioVideoFormats(DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, + DashTestData.WIDEVINE_H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID) + .runTest(getActivity(), getInstrumentation()); } // Internal. - private void testDashPlayback(HostActivity activity, String streamName, String manifestFileName, - String audioFormat, boolean isWidevineEncrypted, String videoMimeType, - boolean canIncludeAdditionalVideoFormats, String... videoFormats) { - testDashPlayback(activity, streamName, null, true, manifestFileName, audioFormat, - isWidevineEncrypted, videoMimeType, canIncludeAdditionalVideoFormats, videoFormats); - } - - private void testDashPlayback(HostActivity activity, String streamName, - ActionSchedule actionSchedule, boolean fullPlaybackNoSeeking, String manifestFileName, - String audioFormat, boolean isWidevineEncrypted, String videoMimeType, - boolean canIncludeAdditionalVideoFormats, String... videoFormats) { - testDashPlayback(activity, streamName, actionSchedule, fullPlaybackNoSeeking, - newDashHostedTestEncParameters(manifestFileName, isWidevineEncrypted, videoMimeType), - audioFormat, canIncludeAdditionalVideoFormats, null, videoFormats); - } - - private void testDashPlayback(HostActivity activity, String streamName, - ActionSchedule actionSchedule, boolean fullPlaybackNoSeeking, - DashHostedTestEncParameters parameters, String audioFormat, - boolean canIncludeAdditionalVideoFormats, byte[] offlineLicenseKeySetId, - String... videoFormats) { - MetricsLogger metricsLogger = MetricsLogger.Factory.createDefault(getInstrumentation(), TAG, - REPORT_NAME, REPORT_OBJECT_NAME); - DashHostedTest test = new DashHostedTest(streamName, metricsLogger, fullPlaybackNoSeeking, - audioFormat, canIncludeAdditionalVideoFormats, false, actionSchedule, parameters, - offlineLicenseKeySetId, videoFormats); - activity.runTest(test, TEST_TIMEOUT_MS); - // Retry test exactly once if adaptive test fails due to excessive dropped buffers when playing - // non-CDD required formats (b/28220076). - if (test.needsCddLimitedRetry) { - metricsLogger = MetricsLogger.Factory.createDefault(getInstrumentation(), TAG, REPORT_NAME, - REPORT_OBJECT_NAME); - test = new DashHostedTest(streamName, metricsLogger, fullPlaybackNoSeeking, audioFormat, - false, true, actionSchedule, parameters, offlineLicenseKeySetId, videoFormats); - activity.runTest(test, TEST_TIMEOUT_MS); - } - } - - private static DashHostedTestEncParameters newDashHostedTestEncParameters(String manifestFileName, - boolean isWidevineEncrypted, String videoMimeType) { - String manifestPath = MANIFEST_URL_PREFIX + manifestFileName; - return new DashHostedTestEncParameters(manifestPath, isWidevineEncrypted, videoMimeType); - } - private static boolean shouldSkipAdaptiveTest(String mimeType) throws DecoderQueryException { MediaCodecInfo decoderInfo = MediaCodecUtil.getDecoderInfo(mimeType, false); assertNotNull(decoderInfo); @@ -778,332 +582,4 @@ public final class DashTest extends ActivityInstrumentationTestCase2 offlineLicenseHelper; - private final DefaultHttpDataSourceFactory httpDataSourceFactory; - private byte[] offlineLicenseKeySetId; - - public TestOfflineLicenseHelper(DashHostedTestEncParameters parameters) - throws UnsupportedDrmException { - this.parameters = parameters; - httpDataSourceFactory = new DefaultHttpDataSourceFactory("ExoPlayerPlaybackTests"); - offlineLicenseHelper = OfflineLicenseHelper.newWidevineInstance( - parameters.widevineLicenseUrl, httpDataSourceFactory); - } - - public byte[] downloadLicense() throws InterruptedException, DrmSessionException, IOException { - assertNull(offlineLicenseKeySetId); - offlineLicenseKeySetId = offlineLicenseHelper - .download(httpDataSourceFactory.createDataSource(), parameters.manifestUrl); - assertNotNull(offlineLicenseKeySetId); - assertTrue(offlineLicenseKeySetId.length > 0); - return offlineLicenseKeySetId; - } - - public void renewLicense() throws DrmSessionException { - assertNotNull(offlineLicenseKeySetId); - offlineLicenseKeySetId = offlineLicenseHelper.renew(offlineLicenseKeySetId); - assertNotNull(offlineLicenseKeySetId); - } - - public void releaseLicense() throws DrmSessionException { - assertNotNull(offlineLicenseKeySetId); - offlineLicenseHelper.release(offlineLicenseKeySetId); - offlineLicenseKeySetId = null; - } - - public Pair getLicenseDurationRemainingSec() throws DrmSessionException { - return offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId); - } - - public void releaseResources() throws DrmSessionException { - if (offlineLicenseKeySetId != null) { - releaseLicense(); - } - if (offlineLicenseHelper != null) { - offlineLicenseHelper.releaseResources(); - } - } - - } - - @TargetApi(16) - private static class DashHostedTest extends ExoHostedTest { - - private final String streamName; - private final MetricsLogger metricsLogger; - private final boolean fullPlaybackNoSeeking; - private final boolean isCddLimitedRetry; - private final DashTestTrackSelector trackSelector; - private final DashHostedTestEncParameters parameters; - private final byte[] offlineLicenseKeySetId; - - private boolean needsCddLimitedRetry; - - /** - * @param streamName The name of the test stream for metric logging. - * @param metricsLogger Logger to log metrics from the test. - * @param fullPlaybackNoSeeking Whether the test will play the entire source with no seeking. - * @param audioFormat The audio format. - * @param canIncludeAdditionalVideoFormats Whether to use video formats in addition to those - * listed in the videoFormats argument, if the device is capable of playing them. - * @param isCddLimitedRetry Whether this is a CDD limited retry following a previous failure. - * @param actionSchedule The action schedule for the test. - * @param parameters Encryption parameters. - * @param offlineLicenseKeySetId The key set id of the license to be used. - * @param videoFormats The video formats. - */ - public DashHostedTest(String streamName, MetricsLogger metricsLogger, - boolean fullPlaybackNoSeeking, String audioFormat, - boolean canIncludeAdditionalVideoFormats, boolean isCddLimitedRetry, - ActionSchedule actionSchedule, DashHostedTestEncParameters parameters, - byte[] offlineLicenseKeySetId, String... videoFormats) { - super(TAG, fullPlaybackNoSeeking); - Assertions.checkArgument(!(isCddLimitedRetry && canIncludeAdditionalVideoFormats)); - this.streamName = streamName; - this.metricsLogger = metricsLogger; - this.fullPlaybackNoSeeking = fullPlaybackNoSeeking; - this.isCddLimitedRetry = isCddLimitedRetry; - this.parameters = parameters; - this.offlineLicenseKeySetId = offlineLicenseKeySetId; - trackSelector = new DashTestTrackSelector(audioFormat, videoFormats, - canIncludeAdditionalVideoFormats); - if (actionSchedule != null) { - setSchedule(actionSchedule); - } - } - - @Override - protected MappingTrackSelector buildTrackSelector(HostActivity host, - BandwidthMeter bandwidthMeter) { - return trackSelector; - } - - @Override - protected final DefaultDrmSessionManager buildDrmSessionManager( - final String userAgent) { - DefaultDrmSessionManager drmSessionManager = null; - if (parameters.isWidevineEncrypted) { - try { - MediaDrmCallback drmCallback = new HttpMediaDrmCallback(parameters.widevineLicenseUrl, - new DefaultHttpDataSourceFactory(userAgent)); - drmSessionManager = DefaultDrmSessionManager.newWidevineInstance(drmCallback, null, - null, null); - if (!parameters.useL1Widevine) { - drmSessionManager.setPropertyString(SECURITY_LEVEL_PROPERTY, WIDEVINE_SECURITY_LEVEL_3); - } - if (offlineLicenseKeySetId != null) { - drmSessionManager.setMode(DefaultDrmSessionManager.MODE_PLAYBACK, - offlineLicenseKeySetId); - } - } catch (UnsupportedDrmException e) { - throw new IllegalStateException(e); - } - } - return drmSessionManager; - } - - @Override - protected SimpleExoPlayer buildExoPlayer(HostActivity host, Surface surface, - MappingTrackSelector trackSelector, - DrmSessionManager drmSessionManager) { - SimpleExoPlayer player = new DebugSimpleExoPlayer(host, trackSelector, - new DefaultLoadControl(), drmSessionManager); - player.setVideoSurface(surface); - return player; - } - - @Override - protected MediaSource buildSource(HostActivity host, String userAgent, - TransferListener mediaTransferListener) { - DataSource.Factory manifestDataSourceFactory = new DefaultDataSourceFactory(host, userAgent); - DataSource.Factory mediaDataSourceFactory = new DefaultDataSourceFactory(host, userAgent, - mediaTransferListener); - Uri manifestUri = Uri.parse(parameters.manifestUrl); - DefaultDashChunkSource.Factory chunkSourceFactory = new DefaultDashChunkSource.Factory( - mediaDataSourceFactory); - return new DashMediaSource(manifestUri, manifestDataSourceFactory, chunkSourceFactory, - MIN_LOADABLE_RETRY_COUNT, 0 /* livePresentationDelayMs */, null, null); - } - - @Override - protected void logMetrics(DecoderCounters audioCounters, DecoderCounters videoCounters) { - metricsLogger.logMetric(MetricsLogger.KEY_TEST_NAME, streamName); - metricsLogger.logMetric(MetricsLogger.KEY_IS_CDD_LIMITED_RETRY, isCddLimitedRetry); - metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_DROPPED_COUNT, - videoCounters.droppedOutputBufferCount); - metricsLogger.logMetric(MetricsLogger.KEY_MAX_CONSECUTIVE_FRAMES_DROPPED_COUNT, - videoCounters.maxConsecutiveDroppedOutputBufferCount); - metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_SKIPPED_COUNT, - videoCounters.skippedOutputBufferCount); - metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_RENDERED_COUNT, - videoCounters.renderedOutputBufferCount); - metricsLogger.close(); - } - - @Override - protected void assertPassed(DecoderCounters audioCounters, DecoderCounters videoCounters) { - if (fullPlaybackNoSeeking) { - // We shouldn't have skipped any output buffers. - DecoderCountersUtil.assertSkippedOutputBufferCount(AUDIO_TAG, audioCounters, 0); - DecoderCountersUtil.assertSkippedOutputBufferCount(VIDEO_TAG, videoCounters, 0); - // We allow one fewer output buffer due to the way that MediaCodecRenderer and the - // underlying decoders handle the end of stream. This should be tightened up in the future. - DecoderCountersUtil.assertTotalOutputBufferCount(AUDIO_TAG, audioCounters, - audioCounters.inputBufferCount - 1, audioCounters.inputBufferCount); - DecoderCountersUtil.assertTotalOutputBufferCount(VIDEO_TAG, videoCounters, - videoCounters.inputBufferCount - 1, videoCounters.inputBufferCount); - } - try { - int droppedFrameLimit = (int) Math.ceil(MAX_DROPPED_VIDEO_FRAME_FRACTION - * DecoderCountersUtil.getTotalOutputBuffers(videoCounters)); - // Assert that performance is acceptable. - // Assert that total dropped frames were within limit. - DecoderCountersUtil.assertDroppedOutputBufferLimit(VIDEO_TAG, videoCounters, - droppedFrameLimit); - // Assert that consecutive dropped frames were within limit. - DecoderCountersUtil.assertConsecutiveDroppedOutputBufferLimit(VIDEO_TAG, videoCounters, - MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES); - } catch (AssertionFailedError e) { - if (trackSelector.includedAdditionalVideoFormats) { - // Retry limiting to CDD mandated formats (b/28220076). - Log.e(TAG, "Too many dropped or consecutive dropped frames.", e); - needsCddLimitedRetry = true; - } else { - throw e; - } - } - } - - } - - private static final class DashTestTrackSelector extends MappingTrackSelector { - - private final String audioFormatId; - private final String[] videoFormatIds; - private final boolean canIncludeAdditionalVideoFormats; - - public boolean includedAdditionalVideoFormats; - - private DashTestTrackSelector(String audioFormatId, String[] videoFormatIds, - boolean canIncludeAdditionalVideoFormats) { - this.audioFormatId = audioFormatId; - this.videoFormatIds = videoFormatIds; - this.canIncludeAdditionalVideoFormats = canIncludeAdditionalVideoFormats; - } - - @Override - protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, - TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) - throws ExoPlaybackException { - Assertions.checkState(rendererCapabilities[VIDEO_RENDERER_INDEX].getTrackType() - == C.TRACK_TYPE_VIDEO); - Assertions.checkState(rendererCapabilities[AUDIO_RENDERER_INDEX].getTrackType() - == C.TRACK_TYPE_AUDIO); - Assertions.checkState(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].length == 1); - Assertions.checkState(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].length == 1); - TrackSelection[] selections = new TrackSelection[rendererCapabilities.length]; - selections[VIDEO_RENDERER_INDEX] = new RandomTrackSelection( - rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0), - getVideoTrackIndices(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0), - rendererFormatSupports[VIDEO_RENDERER_INDEX][0], videoFormatIds, - canIncludeAdditionalVideoFormats), - 0 /* seed */); - selections[AUDIO_RENDERER_INDEX] = new FixedTrackSelection( - rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0), - getTrackIndex(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0), audioFormatId)); - includedAdditionalVideoFormats = - selections[VIDEO_RENDERER_INDEX].length() > videoFormatIds.length; - return selections; - } - - private static int[] getVideoTrackIndices(TrackGroup trackGroup, int[] formatSupport, - String[] formatIds, boolean canIncludeAdditionalFormats) { - List trackIndices = new ArrayList<>(); - - // Always select explicitly listed representations. - for (String formatId : formatIds) { - int trackIndex = getTrackIndex(trackGroup, formatId); - Log.d(TAG, "Adding base video format: " - + Format.toLogString(trackGroup.getFormat(trackIndex))); - trackIndices.add(trackIndex); - } - - // Select additional video representations, if supported by the device. - if (canIncludeAdditionalFormats) { - for (int i = 0; i < trackGroup.length; i++) { - if (!trackIndices.contains(i) && isFormatHandled(formatSupport[i])) { - Log.d(TAG, "Adding extra video format: " - + Format.toLogString(trackGroup.getFormat(i))); - trackIndices.add(i); - } - } - } - - int[] trackIndicesArray = Util.toArray(trackIndices); - Arrays.sort(trackIndicesArray); - return trackIndicesArray; - } - - private static int getTrackIndex(TrackGroup trackGroup, String formatId) { - for (int i = 0; i < trackGroup.length; i++) { - if (trackGroup.getFormat(i).id.equals(formatId)) { - return i; - } - } - throw new IllegalStateException("Format " + formatId + " not found."); - } - - private static boolean isFormatHandled(int formatSupport) { - return (formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK) - == RendererCapabilities.FORMAT_HANDLED; - } - - } - } diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java new file mode 100644 index 0000000000..c95614bc87 --- /dev/null +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2017 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.playbacktests.gts; + +import com.google.android.exoplayer2.util.Util; + +/** + * Test data for {@link DashTest} and {@link DashWidevineOfflineTest). + */ +public final class DashTestData { + + // Clear content manifests. + public static final String H264_MANIFEST = "manifest-h264.mpd"; + public static final String H265_MANIFEST = "manifest-h265.mpd"; + public static final String VP9_MANIFEST = "manifest-vp9.mpd"; + public static final String H264_23_MANIFEST = "manifest-h264-23.mpd"; + public static final String H264_24_MANIFEST = "manifest-h264-24.mpd"; + public static final String H264_29_MANIFEST = "manifest-h264-29.mpd"; + // Widevine encrypted content manifests. + public static final String WIDEVINE_H264_MANIFEST_PREFIX = "manifest-h264-enc"; + public static final String WIDEVINE_H265_MANIFEST_PREFIX = "manifest-h265-enc"; + public static final String WIDEVINE_VP9_MANIFEST_PREFIX = "manifest-vp9-enc"; + public static final String WIDEVINE_H264_23_MANIFEST_PREFIX = "manifest-h264-23-enc"; + public static final String WIDEVINE_H264_24_MANIFEST_PREFIX = "manifest-h264-24-enc"; + public static final String WIDEVINE_H264_29_MANIFEST_PREFIX = "manifest-h264-29-enc"; + + public static final String AAC_AUDIO_REPRESENTATION_ID = "141"; + public static final String H264_BASELINE_240P_VIDEO_REPRESENTATION_ID = "avc-baseline-240"; + public static final String H264_BASELINE_480P_VIDEO_REPRESENTATION_ID = "avc-baseline-480"; + public static final String H264_MAIN_240P_VIDEO_REPRESENTATION_ID = "avc-main-240"; + public static final String H264_MAIN_480P_VIDEO_REPRESENTATION_ID = "avc-main-480"; + // The highest quality H264 format mandated by the Android CDD. + public static final String H264_CDD_FIXED = Util.SDK_INT < 23 + ? H264_BASELINE_480P_VIDEO_REPRESENTATION_ID : H264_MAIN_480P_VIDEO_REPRESENTATION_ID; + // Multiple H264 formats mandated by the Android CDD. Note: The CDD actually mandated main profile + // support from API level 23, but we opt to test only from 24 due to known issues on API level 23 + // when switching between baseline and main profiles on certain devices. + public static final String[] H264_CDD_ADAPTIVE = Util.SDK_INT < 24 + ? new String[] { + H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, + H264_BASELINE_480P_VIDEO_REPRESENTATION_ID} + : new String[] { + H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, + H264_BASELINE_480P_VIDEO_REPRESENTATION_ID, + H264_MAIN_240P_VIDEO_REPRESENTATION_ID, + H264_MAIN_480P_VIDEO_REPRESENTATION_ID}; + + public static final String H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID = + "avc-baseline-480-23"; + public static final String H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID = + "avc-baseline-480-24"; + public static final String H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID = + "avc-baseline-480-29"; + + public static final String H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = "hevc-main-288"; + public static final String H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = "hevc-main-360"; + // The highest quality H265 format mandated by the Android CDD. + public static final String H265_CDD_FIXED = H265_BASELINE_360P_VIDEO_REPRESENTATION_ID; + // Multiple H265 formats mandated by the Android CDD. + public static final String[] H265_CDD_ADAPTIVE = + new String[] { + H265_BASELINE_288P_VIDEO_REPRESENTATION_ID, + H265_BASELINE_360P_VIDEO_REPRESENTATION_ID}; + + public static final String VORBIS_AUDIO_REPRESENTATION_ID = "4"; + public static final String VP9_180P_VIDEO_REPRESENTATION_ID = "0"; + public static final String VP9_360P_VIDEO_REPRESENTATION_ID = "1"; + // The highest quality VP9 format mandated by the Android CDD. + public static final String VP9_CDD_FIXED = VP9_360P_VIDEO_REPRESENTATION_ID; + // Multiple VP9 formats mandated by the Android CDD. + public static final String[] VP9_CDD_ADAPTIVE = + new String[] { + VP9_180P_VIDEO_REPRESENTATION_ID, + VP9_360P_VIDEO_REPRESENTATION_ID}; + + // Widevine encrypted content representation ids. + public static final String WIDEVINE_AAC_AUDIO_REPRESENTATION_ID = "0"; + public static final String WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID = "1"; + public static final String WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID = "2"; + public static final String WIDEVINE_H264_MAIN_240P_VIDEO_REPRESENTATION_ID = "3"; + public static final String WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID = "4"; + // The highest quality H264 format mandated by the Android CDD. + public static final String WIDEVINE_H264_CDD_FIXED = Util.SDK_INT < 23 + ? WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID + : WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID; + // Multiple H264 formats mandated by the Android CDD. Note: The CDD actually mandated main profile + // support from API level 23, but we opt to test only from 24 due to known issues on API level 23 + // when switching between baseline and main profiles on certain devices. + public static final String[] WIDEVINE_H264_CDD_ADAPTIVE = Util.SDK_INT < 24 + ? new String[] { + WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, + WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID} + : new String[] { + WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, + WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID, + WIDEVINE_H264_MAIN_240P_VIDEO_REPRESENTATION_ID, + WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID}; + + public static final String WIDEVINE_H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID = "2"; + public static final String WIDEVINE_H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID = "2"; + public static final String WIDEVINE_H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID = "2"; + + public static final String WIDEVINE_H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = "1"; + public static final String WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = "2"; + // The highest quality H265 format mandated by the Android CDD. + public static final String WIDEVINE_H265_CDD_FIXED = + WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID; + // Multiple H265 formats mandated by the Android CDD. + public static final String[] WIDEVINE_H265_CDD_ADAPTIVE = + new String[] { + WIDEVINE_H265_BASELINE_288P_VIDEO_REPRESENTATION_ID, + WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID}; + + public static final String WIDEVINE_VORBIS_AUDIO_REPRESENTATION_ID = "0"; + public static final String WIDEVINE_VP9_180P_VIDEO_REPRESENTATION_ID = "1"; + public static final String WIDEVINE_VP9_360P_VIDEO_REPRESENTATION_ID = "2"; + // The highest quality VP9 format mandated by the Android CDD. + public static final String WIDEVINE_VP9_CDD_FIXED = VP9_360P_VIDEO_REPRESENTATION_ID; + // Multiple VP9 formats mandated by the Android CDD. + public static final String[] WIDEVINE_VP9_CDD_ADAPTIVE = + new String[] { + WIDEVINE_VP9_180P_VIDEO_REPRESENTATION_ID, + WIDEVINE_VP9_360P_VIDEO_REPRESENTATION_ID}; + + private DashTestData() { + } + +} diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java new file mode 100644 index 0000000000..3bf9508128 --- /dev/null +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2017 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.playbacktests.gts; + +import android.media.MediaDrm.MediaDrmStateException; +import android.test.ActivityInstrumentationTestCase2; +import android.util.Pair; +import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; +import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.drm.OfflineLicenseHelper; +import com.google.android.exoplayer2.playbacktests.util.ActionSchedule; +import com.google.android.exoplayer2.playbacktests.util.HostActivity; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import junit.framework.Assert; + +/** + * Tests Widevine encrypted DASH playbacks using offline keys. + */ +public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCase2 { + + private static final String TAG = "DashWidevineOfflineTest"; + private static final String USER_AGENT = "ExoPlayerPlaybackTests"; + + private DashHostedTest.Builder builder; + private String widevineManifestUrl; + private DefaultHttpDataSourceFactory httpDataSourceFactory; + private OfflineLicenseHelper offlineLicenseHelper; + private byte[] offlineLicenseKeySetId; + + public DashWidevineOfflineTest() { + super(HostActivity.class); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + builder = new DashHostedTest.Builder(TAG) + .setStreamName("test_widevine_h264_fixed_offline") + .setManifestUrlForWidevine(DashTestData.WIDEVINE_H264_MANIFEST_PREFIX, MimeTypes.VIDEO_H264) + .setFullPlaybackNoSeeking(true) + .setCanIncludeAdditionalVideoFormats(false) + .setAudioVideoFormats(DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, + DashTestData.WIDEVINE_H264_CDD_FIXED); + + boolean useL1Widevine = DashHostedTest.isL1WidevineAvailable(MimeTypes.VIDEO_H264); + widevineManifestUrl = DashHostedTest + .getWidevineManifestUrl(DashTestData.WIDEVINE_H264_MANIFEST_PREFIX, useL1Widevine); + String widevineLicenseUrl = DashHostedTest.getWidevineLicenseUrl(useL1Widevine); + httpDataSourceFactory = new DefaultHttpDataSourceFactory(USER_AGENT); + offlineLicenseHelper = OfflineLicenseHelper.newWidevineInstance(widevineLicenseUrl, + httpDataSourceFactory); + } + + @Override + protected void tearDown() throws Exception { + if (offlineLicenseKeySetId != null) { + releaseLicense(); + } + if (offlineLicenseHelper != null) { + offlineLicenseHelper.releaseResources(); + } + super.tearDown(); + } + + // Offline license tests + + public void testWidevineOfflineLicense() throws Exception { + if (Util.SDK_INT < 22) { + return; // Pass. + } + downloadLicense(); + builder.runTest(getActivity(), getInstrumentation()); + + // Renew license after playback should still work + offlineLicenseKeySetId = offlineLicenseHelper.renew(offlineLicenseKeySetId); + Assert.assertNotNull(offlineLicenseKeySetId); + } + + public void testWidevineOfflineReleasedLicense() throws Throwable { + if (Util.SDK_INT < 22) { + return; // Pass. + } + downloadLicense(); + releaseLicense(); // keySetId no longer valid. + + try { + builder.runTest(getActivity(), getInstrumentation()); + fail("Playback should fail because the license has been released."); + } catch (Throwable e) { + // Get the root cause + while (true) { + Throwable cause = e.getCause(); + if (cause == null || cause == e) { + break; + } + e = cause; + } + // It should be a MediaDrmStateException instance + if (!(e instanceof MediaDrmStateException)) { + throw e; + } + } + } + + public void testWidevineOfflineExpiredLicense() throws Exception { + if (Util.SDK_INT < 22) { + return; // Pass. + } + downloadLicense(); + + // Wait until the license expires + long licenseDuration = + offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId).first; + assertTrue("License duration should be less than 30 sec. " + + "Server settings might have changed.", licenseDuration < 30); + while (licenseDuration > 0) { + synchronized (this) { + wait(licenseDuration * 1000 + 2000); + } + long previousDuration = licenseDuration; + licenseDuration = + offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId).first; + assertTrue("License duration should be decreasing.", previousDuration > licenseDuration); + } + + // DefaultDrmSessionManager should renew the license and stream play fine + builder.runTest(getActivity(), getInstrumentation()); + } + + public void testWidevineOfflineLicenseExpiresOnPause() throws Exception { + if (Util.SDK_INT < 22) { + return; // Pass. + } + downloadLicense(); + + // During playback pause until the license expires then continue playback + Pair licenseDurationRemainingSec = + offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId); + long licenseDuration = licenseDurationRemainingSec.first; + assertTrue("License duration should be less than 30 sec. " + + "Server settings might have changed.", licenseDuration < 30); + ActionSchedule schedule = new ActionSchedule.Builder(TAG) + .delay(3000).pause().delay(licenseDuration * 1000 + 2000).play().build(); + + // DefaultDrmSessionManager should renew the license and stream play fine + builder + .setActionSchedule(schedule) + .runTest(getActivity(), getInstrumentation()); + } + + private void downloadLicense() throws InterruptedException, DrmSessionException, IOException { + offlineLicenseKeySetId = offlineLicenseHelper.download( + httpDataSourceFactory.createDataSource(), widevineManifestUrl); + Assert.assertNotNull(offlineLicenseKeySetId); + Assert.assertTrue(offlineLicenseKeySetId.length > 0); + builder.setOfflineLicenseKeySetId(offlineLicenseKeySetId); + } + + private void releaseLicense() throws DrmSessionException { + offlineLicenseHelper.release(offlineLicenseKeySetId); + offlineLicenseKeySetId = null; + } + +} diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java index 7bf8985b64..74262f4422 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java +++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java @@ -63,7 +63,8 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen public static final long EXPECTED_PLAYING_TIME_MEDIA_DURATION_MS = -2; public static final long EXPECTED_PLAYING_TIME_UNSET = -1; - private final String tag; + protected final String tag; + private final boolean failOnPlayerError; private final long expectedPlayingTimeMs; private final DecoderCounters videoDecoderCounters; From 7c1b2beb84f6f706490b3e7a241dce87d22ed370 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 31 Jan 2017 08:13:15 -0800 Subject: [PATCH 012/140] Support dyanmically setting key request headers Issue: #1924 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146120465 --- .../exoplayer2/demo/PlayerActivity.java | 24 +++----- .../exoplayer2/drm/HttpMediaDrmCallback.java | 55 +++++++++++++++++-- .../exoplayer2/drm/OfflineLicenseHelper.java | 2 +- .../exoplayer2/upstream/HttpDataSource.java | 18 +++--- 4 files changed, 68 insertions(+), 31 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index bbfadf34af..6c7b72522a 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -70,8 +70,6 @@ import com.google.android.exoplayer2.util.Util; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; /** @@ -239,19 +237,9 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay if (drmSchemeUuid != null) { String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL); String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES); - Map keyRequestProperties; - if (keyRequestPropertiesArray == null || keyRequestPropertiesArray.length < 2) { - keyRequestProperties = null; - } else { - keyRequestProperties = new HashMap<>(); - for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { - keyRequestProperties.put(keyRequestPropertiesArray[i], - keyRequestPropertiesArray[i + 1]); - } - } try { drmSessionManager = buildDrmSessionManager(drmSchemeUuid, drmLicenseUrl, - keyRequestProperties); + keyRequestPropertiesArray); } catch (UnsupportedDrmException e) { int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME @@ -349,12 +337,18 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay } private DrmSessionManager buildDrmSessionManager(UUID uuid, - String licenseUrl, Map keyRequestProperties) throws UnsupportedDrmException { + String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException { if (Util.SDK_INT < 18) { return null; } HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, - buildHttpDataSourceFactory(false), keyRequestProperties); + buildHttpDataSourceFactory(false)); + if (keyRequestPropertiesArray != null) { + for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { + drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], + keyRequestPropertiesArray[i + 1]); + } + } return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger); } diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index e0c9ca5296..f9d5efffb1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -24,6 +24,8 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.HashMap; @@ -57,21 +59,62 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { } /** + * @deprecated Use {@link HttpMediaDrmCallback#HttpMediaDrmCallback(String, Factory)}. Request + * properties can be set by calling {@link #setKeyRequestProperty(String, String)}. * @param defaultUrl The default license URL. * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. * @param keyRequestProperties Request properties to set when making key requests, or null. */ + @Deprecated public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory, Map keyRequestProperties) { this.dataSourceFactory = dataSourceFactory; this.defaultUrl = defaultUrl; - this.keyRequestProperties = keyRequestProperties; + this.keyRequestProperties = new HashMap<>(); + if (keyRequestProperties != null) { + this.keyRequestProperties.putAll(keyRequestProperties); + } + } + + /** + * Sets a header for key requests made by the callback. + * + * @param name The name of the header field. + * @param value The value of the field. + */ + public void setKeyRequestProperty(String name, String value) { + Assertions.checkNotNull(name); + Assertions.checkNotNull(value); + synchronized (keyRequestProperties) { + keyRequestProperties.put(name, value); + } + } + + /** + * Clears a header for key requests made by the callback. + * + * @param name The name of the header field. + */ + public void clearKeyRequestProperty(String name) { + Assertions.checkNotNull(name); + synchronized (keyRequestProperties) { + keyRequestProperties.remove(name); + } + } + + /** + * Clears all headers for key requests made by the callback. + */ + public void clearAllKeyRequestProperties() { + synchronized (keyRequestProperties) { + keyRequestProperties.clear(); + } } @Override public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); - return executePost(url, new byte[0], null); + return executePost(dataSourceFactory, url, new byte[0], null); } @Override @@ -85,14 +128,14 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { if (C.PLAYREADY_UUID.equals(uuid)) { requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES); } - if (keyRequestProperties != null) { + synchronized (keyRequestProperties) { requestProperties.putAll(keyRequestProperties); } - return executePost(url, request.getData(), requestProperties); + return executePost(dataSourceFactory, url, request.getData(), requestProperties); } - private byte[] executePost(String url, byte[] data, Map requestProperties) - throws IOException { + private static byte[] executePost(HttpDataSource.Factory dataSourceFactory, String url, + byte[] data, Map requestProperties) throws IOException { HttpDataSource dataSource = dataSourceFactory.createDataSource(); if (requestProperties != null) { for (Map.Entry requestProperty : requestProperties.entrySet()) { diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index a11d65d4d3..f4a65931b6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -93,7 +93,7 @@ public final class OfflineLicenseHelper { public static OfflineLicenseHelper newWidevineInstance( String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException { return newWidevineInstance( - new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory, null), null); + new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory), null); } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java index 8df8624102..a988cf1a33 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java @@ -41,8 +41,8 @@ public interface HttpDataSource extends DataSource { HttpDataSource createDataSource(); /** - * Sets a default request header field for {@link HttpDataSource} instances subsequently - * created by the factory. Previously created instances are not affected. + * Sets a default request header for {@link HttpDataSource} instances subsequently created by + * the factory. Previously created instances are not affected. * * @param name The name of the header field. * @param value The value of the field. @@ -50,16 +50,16 @@ public interface HttpDataSource extends DataSource { void setDefaultRequestProperty(String name, String value); /** - * Clears a default request header field for {@link HttpDataSource} instances subsequently - * created by the factory. Previously created instances are not affected. + * Clears a default request header for {@link HttpDataSource} instances subsequently created by + * the factory. Previously created instances are not affected. * * @param name The name of the header field. */ void clearDefaultRequestProperty(String name); /** - * Clears all default request header fields for all {@link HttpDataSource} instances - * subsequently created by the factory. Previously created instances are not affected. + * Clears all default request header for all {@link HttpDataSource} instances subsequently + * created by the factory. Previously created instances are not affected. */ void clearAllDefaultRequestProperties(); @@ -232,7 +232,7 @@ public interface HttpDataSource extends DataSource { int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException; /** - * Sets the value of a request header field. The value will be used for subsequent connections + * Sets the value of a request header. The value will be used for subsequent connections * established by the source. * * @param name The name of the header field. @@ -241,7 +241,7 @@ public interface HttpDataSource extends DataSource { void setRequestProperty(String name, String value); /** - * Clears the value of a request header field. The change will apply to subsequent connections + * Clears the value of a request header. The change will apply to subsequent connections * established by the source. * * @param name The name of the header field. @@ -249,7 +249,7 @@ public interface HttpDataSource extends DataSource { void clearRequestProperty(String name); /** - * Clears all request header fields that were set by {@link #setRequestProperty(String, String)}. + * Clears all request headers that were set by {@link #setRequestProperty(String, String)}. */ void clearAllRequestProperties(); From 3edeec2495d9ca2c7f60b9edb212ecf5e6b40103 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 31 Jan 2017 09:28:16 -0800 Subject: [PATCH 013/140] Document passing null cacheWriteDataSink to CacheDataSource constructor ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146128328 --- .../upstream/cache/CacheDataSourceTest.java | 27 ++++++++++++++----- .../upstream/cache/CacheDataSource.java | 13 ++++++--- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index 067cfe4fcd..a5b272cebd 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -20,9 +20,9 @@ import android.test.InstrumentationTestCase; import android.test.MoreAsserts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.FakeDataSource; -import com.google.android.exoplayer2.testutil.FakeDataSource.Builder; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.FileDataSource; import java.io.File; import java.io.IOException; import java.util.Arrays; @@ -126,9 +126,15 @@ public class CacheDataSourceTest extends InstrumentationTestCase { MoreAsserts.assertEmpty(simpleCache.getKeys()); } + public void testReadOnlyCache() throws Exception { + CacheDataSource cacheDataSource = createCacheDataSource(false, false, 0, null); + assertReadDataContentLength(cacheDataSource, false, false); + assertEquals(0, cacheDir.list().length); + } + private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength) throws IOException { - // Read all data from upstream and cache + // Read all data from upstream and write to cache CacheDataSource cacheDataSource = createCacheDataSource(false, simulateUnknownLength); assertReadDataContentLength(cacheDataSource, unboundedRequest, simulateUnknownLength); @@ -184,14 +190,21 @@ public class CacheDataSourceTest extends InstrumentationTestCase { private CacheDataSource createCacheDataSource(boolean setReadException, boolean simulateUnknownLength, @CacheDataSource.Flags int flags) { - Builder builder = new Builder(); + return createCacheDataSource(setReadException, simulateUnknownLength, flags, + new CacheDataSink(simpleCache, MAX_CACHE_FILE_SIZE)); + } + + private CacheDataSource createCacheDataSource(boolean setReadException, + boolean simulateUnknownLength, @CacheDataSource.Flags int flags, + CacheDataSink cacheWriteDataSink) { + FakeDataSource.Builder builder = new FakeDataSource.Builder(); if (setReadException) { builder.appendReadError(new IOException("Shouldn't read from upstream")); } - builder.setSimulateUnknownLength(simulateUnknownLength); - builder.appendReadData(TEST_DATA); - FakeDataSource upstream = builder.build(); - return new CacheDataSource(simpleCache, upstream, flags, MAX_CACHE_FILE_SIZE); + FakeDataSource upstream = + builder.setSimulateUnknownLength(simulateUnknownLength).appendReadData(TEST_DATA).build(); + return new CacheDataSource(simpleCache, upstream, new FileDataSource(), cacheWriteDataSink, + flags, null); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index 9b29984d06..dc8797362f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -142,7 +142,8 @@ public final class CacheDataSource implements DataSource { * @param cache The cache. * @param upstream A {@link DataSource} for reading data not in the cache. * @param cacheReadDataSource A {@link DataSource} for reading data from the cache. - * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. + * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If null, cache is + * accessed read-only. * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link * #FLAG_IGNORE_CACHE_ON_ERROR} or 0. * @param eventListener An optional {@link EventListener} to receive events. @@ -283,7 +284,6 @@ public final class CacheDataSource implements DataSource { currentDataSource = cacheReadDataSource; } else { // Data is not cached, and data is not locked, read from upstream with cache backing. - lockedSpan = span; long length; if (span.isOpenEnded()) { length = bytesRemaining; @@ -294,8 +294,13 @@ public final class CacheDataSource implements DataSource { } } dataSpec = new DataSpec(uri, readPosition, length, key, flags); - currentDataSource = cacheWriteDataSource != null ? cacheWriteDataSource - : upstreamDataSource; + if (cacheWriteDataSource != null) { + currentDataSource = cacheWriteDataSource; + lockedSpan = span; + } else { + currentDataSource = upstreamDataSource; + cache.releaseHoleSpan(span); + } } currentRequestUnbounded = dataSpec.length == C.LENGTH_UNSET; From 4301606200d63462a109a94233b304839bbcc8b0 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 31 Jan 2017 09:29:09 -0800 Subject: [PATCH 014/140] Add a BufferProcessor for resampling. This initial version of the BufferProcessor interface assumes that buffers are handled in their entirety on each invocation. Move PCM resampling out of AudioTrack into a BufferProcessor. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146128411 --- .../android/exoplayer2/audio/AudioTrack.java | 215 ++++++------------ .../exoplayer2/audio/BufferProcessor.java | 37 +++ .../audio/ResamplingBufferProcessor.java | 112 +++++++++ 3 files changed, 218 insertions(+), 146 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index 71049c9de8..11c388fdab 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -25,7 +25,6 @@ import android.os.ConditionVariable; import android.os.SystemClock; import android.util.Log; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -271,9 +270,9 @@ public final class AudioTrack { @C.StreamType private int streamType; @C.Encoding - private int sourceEncoding; + private int inputEncoding; @C.Encoding - private int targetEncoding; + private int outputEncoding; private boolean passthrough; private int pcmFrameSize; private int bufferSize; @@ -299,12 +298,12 @@ public final class AudioTrack { private long latencyUs; private float volume; - private byte[] temporaryBuffer; - private int temporaryBufferOffset; - private ByteBuffer currentSourceBuffer; + private ByteBuffer inputBuffer; + private ByteBuffer outputBuffer; + private byte[] preV21OutputBuffer; + private int preV21OutputBufferOffset; - private ByteBuffer resampledBuffer; - private boolean useResampledBuffer; + private BufferProcessor resampler; private boolean playing; private int audioSessionId; @@ -470,17 +469,17 @@ public final class AudioTrack { channelConfig = AudioFormat.CHANNEL_OUT_STEREO; } - @C.Encoding int sourceEncoding; + @C.Encoding int inputEncoding; if (passthrough) { - sourceEncoding = getEncodingForMimeType(mimeType); + inputEncoding = getEncodingForMimeType(mimeType); } else if (pcmEncoding == C.ENCODING_PCM_8BIT || pcmEncoding == C.ENCODING_PCM_16BIT || pcmEncoding == C.ENCODING_PCM_24BIT || pcmEncoding == C.ENCODING_PCM_32BIT) { - sourceEncoding = pcmEncoding; + inputEncoding = pcmEncoding; } else { throw new IllegalArgumentException("Unsupported PCM encoding: " + pcmEncoding); } - if (isInitialized() && this.sourceEncoding == sourceEncoding && this.sampleRate == sampleRate + if (isInitialized() && this.inputEncoding == inputEncoding && this.sampleRate == sampleRate && this.channelConfig == channelConfig) { // We already have an audio track with the correct sample rate, channel config and encoding. return; @@ -488,28 +487,31 @@ public final class AudioTrack { reset(); - this.sourceEncoding = sourceEncoding; + this.inputEncoding = inputEncoding; this.passthrough = passthrough; this.sampleRate = sampleRate; this.channelConfig = channelConfig; - targetEncoding = passthrough ? sourceEncoding : C.ENCODING_PCM_16BIT; pcmFrameSize = 2 * channelCount; // 2 bytes per 16-bit sample * number of channels. + outputEncoding = passthrough ? inputEncoding : C.ENCODING_PCM_16BIT; + + resampler = outputEncoding != inputEncoding ? new ResamplingBufferProcessor(inputEncoding) + : null; if (specifiedBufferSize != 0) { bufferSize = specifiedBufferSize; } else if (passthrough) { // TODO: Set the minimum buffer size using getMinBufferSize when it takes the encoding into // account. [Internal: b/25181305] - if (targetEncoding == C.ENCODING_AC3 || targetEncoding == C.ENCODING_E_AC3) { + if (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3) { // AC-3 allows bitrates up to 640 kbit/s. bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 80 * 1024 / C.MICROS_PER_SECOND); - } else /* (targetEncoding == C.ENCODING_DTS || targetEncoding == C.ENCODING_DTS_HD */ { + } else /* (outputEncoding == C.ENCODING_DTS || outputEncoding == C.ENCODING_DTS_HD */ { // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s. bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND); } } else { int minBufferSize = - android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, targetEncoding); + android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding); Assertions.checkState(minBufferSize != ERROR_BAD_VALUE); int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR; int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * pcmFrameSize; @@ -531,15 +533,15 @@ public final class AudioTrack { releasingConditionVariable.block(); if (tunneling) { - audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, targetEncoding, + audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, outputEncoding, bufferSize, audioSessionId); } else if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) { audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, - targetEncoding, bufferSize, MODE_STREAM); + outputEncoding, bufferSize, MODE_STREAM); } else { // Re-attach to the same audio session. audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, - targetEncoding, bufferSize, MODE_STREAM, audioSessionId); + outputEncoding, bufferSize, MODE_STREAM, audioSessionId); } checkAudioTrackInitialized(); @@ -611,8 +613,10 @@ public final class AudioTrack { * @throws InitializationException If an error occurs initializing the track. * @throws WriteException If an error occurs writing the audio data. */ + @SuppressWarnings("ReferenceEquality") public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs) throws InitializationException, WriteException { + Assertions.checkArgument(inputBuffer == null || buffer == inputBuffer); if (!isInitialized()) { initialize(); if (playing) { @@ -620,27 +624,12 @@ public final class AudioTrack { } } - boolean hadData = hasData; - hasData = hasPendingData(); - if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) { - long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; - listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs); - } - boolean result = writeBuffer(buffer, presentationTimeUs); - lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); - return result; - } - - @SuppressWarnings("ReferenceEquality") - private boolean writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException { - boolean isNewSourceBuffer = currentSourceBuffer == null; - Assertions.checkState(isNewSourceBuffer || currentSourceBuffer == buffer); - currentSourceBuffer = buffer; - if (needsPassthroughWorkarounds()) { // An AC-3 audio track continues to play data written while it is paused. Stop writing so its // buffer empties. See [Internal: b/18899620]. if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) { + // We force an underrun to pause the track, so don't notify the listener in this case. + hasData = false; return false; } @@ -653,27 +642,25 @@ public final class AudioTrack { } } - if (isNewSourceBuffer) { - // We're seeing this buffer for the first time. + boolean hadData = hasData; + hasData = hasPendingData(); + if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) { + long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; + listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs); + } - if (!currentSourceBuffer.hasRemaining()) { + if (inputBuffer == null) { + // We are seeing this buffer for the first time. + if (!buffer.hasRemaining()) { // The buffer is empty. - currentSourceBuffer = null; return true; } - useResampledBuffer = targetEncoding != sourceEncoding; - if (useResampledBuffer) { - Assertions.checkState(targetEncoding == C.ENCODING_PCM_16BIT); - // Resample the buffer to get the data in the target encoding. - resampledBuffer = resampleTo16BitPcm(currentSourceBuffer, sourceEncoding, resampledBuffer); - buffer = resampledBuffer; - } - if (passthrough && framesPerEncodedSample == 0) { // If this is the first encoded sample, calculate the sample size in frames. - framesPerEncodedSample = getFramesPerEncodedSample(targetEncoding, buffer); + framesPerEncodedSample = getFramesPerEncodedSample(outputEncoding, buffer); } + if (startMediaTimeState == START_NOT_SET) { startMediaTimeUs = Math.max(0, presentationTimeUs); startMediaTimeState = START_IN_SYNC; @@ -695,21 +682,31 @@ public final class AudioTrack { listener.onPositionDiscontinuity(); } } + + inputBuffer = buffer; + outputBuffer = resampler != null ? resampler.handleBuffer(inputBuffer, outputBuffer) + : inputBuffer; if (Util.SDK_INT < 21) { - // Copy {@code buffer} into {@code temporaryBuffer}. - int bytesRemaining = buffer.remaining(); - if (temporaryBuffer == null || temporaryBuffer.length < bytesRemaining) { - temporaryBuffer = new byte[bytesRemaining]; + int bytesRemaining = outputBuffer.remaining(); + if (preV21OutputBuffer == null || preV21OutputBuffer.length < bytesRemaining) { + preV21OutputBuffer = new byte[bytesRemaining]; } - int originalPosition = buffer.position(); - buffer.get(temporaryBuffer, 0, bytesRemaining); - buffer.position(originalPosition); - temporaryBufferOffset = 0; + int originalPosition = outputBuffer.position(); + outputBuffer.get(preV21OutputBuffer, 0, bytesRemaining); + outputBuffer.position(originalPosition); + preV21OutputBufferOffset = 0; } } - buffer = useResampledBuffer ? resampledBuffer : buffer; - int bytesRemaining = buffer.remaining(); + if (writeOutputBuffer(presentationTimeUs)) { + inputBuffer = null; + return true; + } + return false; + } + + private boolean writeOutputBuffer(long presentationTimeUs) throws WriteException { + int bytesRemaining = outputBuffer.remaining(); int bytesWritten = 0; if (Util.SDK_INT < 21) { // passthrough == false // Work out how many bytes we can write without the risk of blocking. @@ -718,18 +715,21 @@ public final class AudioTrack { int bytesToWrite = bufferSize - bytesPending; if (bytesToWrite > 0) { bytesToWrite = Math.min(bytesRemaining, bytesToWrite); - bytesWritten = audioTrack.write(temporaryBuffer, temporaryBufferOffset, bytesToWrite); - if (bytesWritten >= 0) { - temporaryBufferOffset += bytesWritten; + bytesWritten = audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite); + if (bytesWritten > 0) { + preV21OutputBufferOffset += bytesWritten; + outputBuffer.position(outputBuffer.position() + bytesWritten); } - buffer.position(buffer.position() + bytesWritten); } + } else if (tunneling) { + bytesWritten = writeNonBlockingWithAvSyncV21(audioTrack, outputBuffer, bytesRemaining, + presentationTimeUs); } else { - bytesWritten = tunneling - ? writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, presentationTimeUs) - : writeNonBlockingV21(audioTrack, buffer, bytesRemaining); + bytesWritten = writeNonBlockingV21(audioTrack, outputBuffer, bytesRemaining); } + lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); + if (bytesWritten < 0) { throw new WriteException(bytesWritten); } @@ -741,7 +741,6 @@ public final class AudioTrack { if (passthrough) { submittedEncodedFrames += framesPerEncodedSample; } - currentSourceBuffer = null; return true; } return false; @@ -885,7 +884,7 @@ public final class AudioTrack { submittedPcmBytes = 0; submittedEncodedFrames = 0; framesPerEncodedSample = 0; - currentSourceBuffer = null; + inputBuffer = null; avSyncHeader = null; bytesUntilNextAvSync = 0; startMediaTimeState = START_NOT_SET; @@ -1094,7 +1093,7 @@ public final class AudioTrack { */ private boolean needsPassthroughWorkarounds() { return Util.SDK_INT < 23 - && (targetEncoding == C.ENCODING_AC3 || targetEncoding == C.ENCODING_E_AC3); + && (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3); } /** @@ -1129,82 +1128,6 @@ public final class AudioTrack { sessionId); } - /** - * Converts the provided buffer into 16-bit PCM. - * - * @param buffer The buffer containing the data to convert. - * @param sourceEncoding The data encoding. - * @param out A buffer into which the output should be written, if its capacity is sufficient. - * @return The 16-bit PCM output. Different to the out parameter if null was passed, or if the - * capacity was insufficient for the output. - */ - private static ByteBuffer resampleTo16BitPcm(ByteBuffer buffer, @C.PcmEncoding int sourceEncoding, - ByteBuffer out) { - int offset = buffer.position(); - int limit = buffer.limit(); - int size = limit - offset; - - int resampledSize; - switch (sourceEncoding) { - case C.ENCODING_PCM_8BIT: - resampledSize = size * 2; - break; - case C.ENCODING_PCM_24BIT: - resampledSize = (size / 3) * 2; - break; - case C.ENCODING_PCM_32BIT: - resampledSize = size / 2; - break; - case C.ENCODING_PCM_16BIT: - case C.ENCODING_INVALID: - case Format.NO_VALUE: - default: - // Never happens. - throw new IllegalStateException(); - } - - ByteBuffer resampledBuffer = out; - if (resampledBuffer == null || resampledBuffer.capacity() < resampledSize) { - resampledBuffer = ByteBuffer.allocateDirect(resampledSize); - } - resampledBuffer.position(0); - resampledBuffer.limit(resampledSize); - - // Samples are little endian. - switch (sourceEncoding) { - case C.ENCODING_PCM_8BIT: - // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. - for (int i = offset; i < limit; i++) { - resampledBuffer.put((byte) 0); - resampledBuffer.put((byte) ((buffer.get(i) & 0xFF) - 128)); - } - break; - case C.ENCODING_PCM_24BIT: - // 24->16 bit resampling. Drop the least significant byte. - for (int i = offset; i < limit; i += 3) { - resampledBuffer.put(buffer.get(i + 1)); - resampledBuffer.put(buffer.get(i + 2)); - } - break; - case C.ENCODING_PCM_32BIT: - // 32->16 bit resampling. Drop the two least significant bytes. - for (int i = offset; i < limit; i += 4) { - resampledBuffer.put(buffer.get(i + 2)); - resampledBuffer.put(buffer.get(i + 3)); - } - break; - case C.ENCODING_PCM_16BIT: - case C.ENCODING_INVALID: - case Format.NO_VALUE: - default: - // Never happens. - throw new IllegalStateException(); - } - - resampledBuffer.position(0); - return resampledBuffer; - } - @C.Encoding private static int getEncodingForMimeType(String mimeType) { switch (mimeType) { diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java new file mode 100644 index 0000000000..a10e8c05af --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2017 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.audio; + +import java.nio.ByteBuffer; + +/** + * Interface for processors of buffers, for use with {@link AudioTrack}. + */ +public interface BufferProcessor { + + /** + * Processes the data in the specified input buffer in its entirety. Populates {@code output} with + * processed data if is not {@code null} and has sufficient capacity. Otherwise a different buffer + * will be populated and returned. + * + * @param input A buffer containing the input data to process. + * @param output A buffer into which the output should be written, if its capacity is sufficient. + * @return The processed output. Different to {@code output} if null was passed, or if its + * capacity was insufficient. + */ + ByteBuffer handleBuffer(ByteBuffer input, ByteBuffer output); + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java new file mode 100644 index 0000000000..f0ea5e60c7 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2017 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.audio; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.util.Assertions; +import java.nio.ByteBuffer; + +/** + * A {@link BufferProcessor} that converts PCM input buffers from a specified input bit depth to + * {@link C#ENCODING_PCM_16BIT} in preparation for writing to an {@link android.media.AudioTrack}. + */ +/* package */ final class ResamplingBufferProcessor implements BufferProcessor { + + @C.PcmEncoding + private final int inputEncoding; + + /** + * Creates a new buffer processor for resampling input in the specified encoding. + * + * @param inputEncoding The PCM encoding of input buffers. + * @throws IllegalArgumentException Thrown if the input encoding is not PCM or its bit depth is + * not 8, 24 or 32-bits. + */ + public ResamplingBufferProcessor(@C.PcmEncoding int inputEncoding) { + Assertions.checkArgument(inputEncoding == C.ENCODING_PCM_8BIT + || inputEncoding == C.ENCODING_PCM_24BIT || inputEncoding == C.ENCODING_PCM_32BIT); + this.inputEncoding = inputEncoding; + } + + @Override + public ByteBuffer handleBuffer(ByteBuffer input, ByteBuffer output) { + int offset = input.position(); + int limit = input.limit(); + int size = limit - offset; + + int resampledSize; + switch (inputEncoding) { + case C.ENCODING_PCM_8BIT: + resampledSize = size * 2; + break; + case C.ENCODING_PCM_24BIT: + resampledSize = (size / 3) * 2; + break; + case C.ENCODING_PCM_32BIT: + resampledSize = size / 2; + break; + case C.ENCODING_PCM_16BIT: + case C.ENCODING_INVALID: + case Format.NO_VALUE: + default: + // Never happens. + throw new IllegalStateException(); + } + + ByteBuffer resampledBuffer = output; + if (resampledBuffer == null || resampledBuffer.capacity() < resampledSize) { + resampledBuffer = ByteBuffer.allocateDirect(resampledSize); + } + resampledBuffer.position(0); + resampledBuffer.limit(resampledSize); + + // Samples are little endian. + switch (inputEncoding) { + case C.ENCODING_PCM_8BIT: + // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. + for (int i = offset; i < limit; i++) { + resampledBuffer.put((byte) 0); + resampledBuffer.put((byte) ((input.get(i) & 0xFF) - 128)); + } + break; + case C.ENCODING_PCM_24BIT: + // 24->16 bit resampling. Drop the least significant byte. + for (int i = offset; i < limit; i += 3) { + resampledBuffer.put(input.get(i + 1)); + resampledBuffer.put(input.get(i + 2)); + } + break; + case C.ENCODING_PCM_32BIT: + // 32->16 bit resampling. Drop the two least significant bytes. + for (int i = offset; i < limit; i += 4) { + resampledBuffer.put(input.get(i + 2)); + resampledBuffer.put(input.get(i + 3)); + } + break; + case C.ENCODING_PCM_16BIT: + case C.ENCODING_INVALID: + case Format.NO_VALUE: + default: + // Never happens. + throw new IllegalStateException(); + } + + resampledBuffer.position(0); + return resampledBuffer; + } + +} From feeec77407f8c5878ce4294039520d788be45c9d Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 31 Jan 2017 12:33:43 -0800 Subject: [PATCH 015/140] Add support for multiple programs in a single TS * Prevents calling endTracks() before all PMTs have been processed. * Adds a unique ID to the format of each track. This allows the track selector to identify which track belongs to each program. The format of each id is "/". Note: This CL will break malformed TS files whose PAT declares more PMTs than it actually contains, which previously were supported. This does not apply for HLS mode. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146151642 --- .../androidTest/assets/ts/sample.ac3.0.dump | 2 +- .../androidTest/assets/ts/sample.adts.0.dump | 4 +- .../androidTest/assets/ts/sample.ps.0.dump | 4 +- .../androidTest/assets/ts/sample.ts.0.dump | 4 +- .../extractor/ts/TsExtractorTest.java | 7 ++- .../exoplayer2/extractor/ts/Ac3Reader.java | 10 ++- .../exoplayer2/extractor/ts/AdtsReader.java | 14 +++-- .../exoplayer2/extractor/ts/DtsReader.java | 7 ++- .../exoplayer2/extractor/ts/H262Reader.java | 12 ++-- .../exoplayer2/extractor/ts/H264Reader.java | 11 +++- .../exoplayer2/extractor/ts/H265Reader.java | 17 +++-- .../exoplayer2/extractor/ts/Id3Reader.java | 7 ++- .../extractor/ts/MpegAudioReader.java | 11 ++-- .../exoplayer2/extractor/ts/SeiReader.java | 4 +- .../extractor/ts/SpliceInfoSectionReader.java | 7 ++- .../exoplayer2/extractor/ts/TsExtractor.java | 63 +++++++++++++------ .../extractor/ts/TsPayloadReader.java | 62 +++++++++++++++--- .../exoplayer2/util/TimestampAdjuster.java | 2 +- 18 files changed, 175 insertions(+), 73 deletions(-) diff --git a/library/src/androidTest/assets/ts/sample.ac3.0.dump b/library/src/androidTest/assets/ts/sample.ac3.0.dump index c5f241950b..1b6c77efb6 100644 --- a/library/src/androidTest/assets/ts/sample.ac3.0.dump +++ b/library/src/androidTest/assets/ts/sample.ac3.0.dump @@ -6,7 +6,7 @@ numberOfTracks = 1 track 0: format: bitrate = -1 - id = null + id = 0 containerMimeType = null sampleMimeType = audio/ac3 maxInputSize = -1 diff --git a/library/src/androidTest/assets/ts/sample.adts.0.dump b/library/src/androidTest/assets/ts/sample.adts.0.dump index 3325abcfeb..0a7427d3f1 100644 --- a/library/src/androidTest/assets/ts/sample.adts.0.dump +++ b/library/src/androidTest/assets/ts/sample.adts.0.dump @@ -6,7 +6,7 @@ numberOfTracks = 2 track 0: format: bitrate = -1 - id = null + id = 0 containerMimeType = null sampleMimeType = audio/mp4a-latm maxInputSize = -1 @@ -606,7 +606,7 @@ track 0: track 1: format: bitrate = -1 - id = null + id = 1 containerMimeType = null sampleMimeType = application/id3 maxInputSize = -1 diff --git a/library/src/androidTest/assets/ts/sample.ps.0.dump b/library/src/androidTest/assets/ts/sample.ps.0.dump index 48127ce1c6..3b44fb6fb9 100644 --- a/library/src/androidTest/assets/ts/sample.ps.0.dump +++ b/library/src/androidTest/assets/ts/sample.ps.0.dump @@ -6,7 +6,7 @@ numberOfTracks = 2 track 192: format: bitrate = -1 - id = null + id = 192 containerMimeType = null sampleMimeType = audio/mpeg-L2 maxInputSize = 4096 @@ -45,7 +45,7 @@ track 192: track 224: format: bitrate = -1 - id = null + id = 224 containerMimeType = null sampleMimeType = video/mpeg2 maxInputSize = -1 diff --git a/library/src/androidTest/assets/ts/sample.ts.0.dump b/library/src/androidTest/assets/ts/sample.ts.0.dump index 8b0da7bd02..26c6665aaa 100644 --- a/library/src/androidTest/assets/ts/sample.ts.0.dump +++ b/library/src/androidTest/assets/ts/sample.ts.0.dump @@ -6,7 +6,7 @@ numberOfTracks = 2 track 256: format: bitrate = -1 - id = null + id = 1/256 containerMimeType = null sampleMimeType = video/mpeg2 maxInputSize = -1 @@ -38,7 +38,7 @@ track 256: track 257: format: bitrate = -1 - id = null + id = 1/257 containerMimeType = null sampleMimeType = audio/mpeg-L2 maxInputSize = 4096 diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index 2dce742158..74e0748119 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -92,7 +92,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { TrackOutput trackOutput = reader.getTrackOutput(); assertTrue(trackOutput == output.trackOutputs.get(257 /* PID of audio track. */)); assertEquals( - Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0, "und", null, 0), + Format.createTextSampleFormat("1/257", "mime", null, 0, 0, "und", null, 0), ((FakeTrackOutput) trackOutput).format); } @@ -178,8 +178,9 @@ public final class TsExtractorTest extends InstrumentationTestCase { @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); - output.format(Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0, + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId()); + output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), "mime", null, 0, 0, language, null, 0)); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java index 52faa8c673..afef154ed4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private final ParsableByteArray headerScratchBytes; private final String language; + private String trackFormatId; private TrackOutput output; private int state; @@ -84,7 +85,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { - output = extractorOutput.track(generator.getNextId()); + generator.generateNewId(); + trackFormatId = generator.getFormatId(); + output = extractorOutput.track(generator.getTrackId()); } @Override @@ -180,8 +183,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; headerScratchBits.skipBits(40); isEac3 = headerScratchBits.readBits(5) == 16; headerScratchBits.setPosition(headerScratchBits.getPosition() - 45); - format = isEac3 ? Ac3Util.parseEac3SyncframeFormat(headerScratchBits, null, language , null) - : Ac3Util.parseAc3SyncframeFormat(headerScratchBits, null, language, null); + format = isEac3 + ? Ac3Util.parseEac3SyncframeFormat(headerScratchBits, trackFormatId, language , null) + : Ac3Util.parseAc3SyncframeFormat(headerScratchBits, trackFormatId, language, null); output.format(format); } sampleSize = isEac3 ? Ac3Util.parseEAc3SyncframeSize(headerScratchBits.data) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java index 47cb217fc7..56793119e4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java @@ -61,6 +61,7 @@ import java.util.Collections; private final ParsableByteArray id3HeaderBuffer; private final String language; + private String formatId; private TrackOutput output; private TrackOutput id3Output; @@ -108,11 +109,14 @@ import java.util.Collections; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId()); if (exposeId3) { - id3Output = extractorOutput.track(idGenerator.getNextId()); - id3Output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null, - Format.NO_VALUE, null)); + idGenerator.generateNewId(); + id3Output = extractorOutput.track(idGenerator.getTrackId()); + id3Output.format(Format.createSampleFormat(idGenerator.getFormatId(), + MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, null)); } else { id3Output = new DummyTrackOutput(); } @@ -300,7 +304,7 @@ import java.util.Collections; Pair audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig( audioSpecificConfig); - Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null, + Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first, Collections.singletonList(audioSpecificConfig), null, 0, language); // In this class a sample is an access unit, but the MediaFormat sample rate specifies the diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java index 9707685295..50be258ae5 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private final ParsableByteArray headerScratchBytes; private final String language; + private String formatId; private TrackOutput output; private int state; @@ -79,7 +80,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId()); } @Override @@ -165,7 +168,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private void parseHeader() { byte[] frameData = headerScratchBytes.data; if (format == null) { - format = DtsUtil.parseDtsFormat(frameData, null, language, null); + format = DtsUtil.parseDtsFormat(frameData, formatId, language, null); output.format(format); } sampleSize = DtsUtil.getDtsFrameSize(frameData); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index 02ea6d7c4e..df6ba208c3 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -37,6 +37,7 @@ import java.util.Collections; private static final int START_EXTENSION = 0xB5; private static final int START_GROUP = 0xB8; + private String formatId; private TrackOutput output; // Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4. @@ -78,7 +79,9 @@ import java.util.Collections; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId()); } @Override @@ -126,7 +129,7 @@ import java.util.Collections; int bytesAlreadyPassed = lengthToStartCode < 0 ? -lengthToStartCode : 0; if (csdBuffer.onStartCode(startCodeValue, bytesAlreadyPassed)) { // The csd data is complete, so we can decode and output the media format. - Pair result = parseCsdBuffer(csdBuffer); + Pair result = parseCsdBuffer(csdBuffer, formatId); output.format(result.first); frameDurationUs = result.second; hasOutputFormat = true; @@ -166,10 +169,11 @@ import java.util.Collections; * Parses the {@link Format} and frame duration from a csd buffer. * * @param csdBuffer The csd buffer. + * @param formatId The id for the generated format. May be null. * @return A pair consisting of the {@link Format} and the frame duration in microseconds, or * 0 if the duration could not be determined. */ - private static Pair parseCsdBuffer(CsdBuffer csdBuffer) { + private static Pair parseCsdBuffer(CsdBuffer csdBuffer, String formatId) { byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length); int firstByte = csdData[4] & 0xFF; @@ -195,7 +199,7 @@ import java.util.Collections; break; } - Format format = Format.createVideoSampleFormat(null, MimeTypes.VIDEO_MPEG2, null, + Format format = Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_MPEG2, null, Format.NO_VALUE, Format.NO_VALUE, width, height, Format.NO_VALUE, Collections.singletonList(csdData), Format.NO_VALUE, pixelWidthHeightRatio, null); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java index ed4682d9b9..0de6bdeaf9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java @@ -47,6 +47,7 @@ import java.util.List; private long totalBytesWritten; private final boolean[] prefixFlags; + private String formatId; private TrackOutput output; private SeiReader seiReader; private SampleReader sampleReader; @@ -88,9 +89,13 @@ import java.util.List; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId()); sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits); - seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId())); + idGenerator.generateNewId(); + seiReader = new SeiReader(extractorOutput.track(idGenerator.getTrackId()), + idGenerator.getFormatId()); } @Override @@ -175,7 +180,7 @@ import java.util.List; initializationData.add(Arrays.copyOf(pps.nalData, pps.nalLength)); NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength); NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength); - output.format(Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, null, + output.format(Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, spsData.width, spsData.height, Format.NO_VALUE, initializationData, Format.NO_VALUE, spsData.pixelWidthAspectRatio, null)); hasOutputFormat = true; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index a78169a054..0f8a7745a5 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -44,6 +44,7 @@ import java.util.Collections; private static final int PREFIX_SEI_NUT = 39; private static final int SUFFIX_SEI_NUT = 40; + private String formatId; private TrackOutput output; private SampleReader sampleReader; private SeiReader seiReader; @@ -90,9 +91,13 @@ import java.util.Collections; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId()); sampleReader = new SampleReader(output); - seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId())); + idGenerator.generateNewId(); + seiReader = new SeiReader(extractorOutput.track(idGenerator.getTrackId()), + idGenerator.getFormatId()); } @Override @@ -183,7 +188,7 @@ import java.util.Collections; sps.endNalUnit(discardPadding); pps.endNalUnit(discardPadding); if (vps.isCompleted() && sps.isCompleted() && pps.isCompleted()) { - output.format(parseMediaFormat(vps, sps, pps)); + output.format(parseMediaFormat(formatId, vps, sps, pps)); hasOutputFormat = true; } } @@ -205,8 +210,8 @@ import java.util.Collections; } } - private static Format parseMediaFormat(NalUnitTargetBuffer vps, NalUnitTargetBuffer sps, - NalUnitTargetBuffer pps) { + private static Format parseMediaFormat(String formatId, NalUnitTargetBuffer vps, + NalUnitTargetBuffer sps, NalUnitTargetBuffer pps) { // Build codec-specific data. byte[] csd = new byte[vps.nalLength + sps.nalLength + pps.nalLength]; System.arraycopy(vps.nalData, 0, csd, 0, vps.nalLength); @@ -311,7 +316,7 @@ import java.util.Collections; } } - return Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H265, null, Format.NO_VALUE, + return Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H265, null, Format.NO_VALUE, Format.NO_VALUE, picWidthInLumaSamples, picHeightInLumaSamples, Format.NO_VALUE, Collections.singletonList(csd), Format.NO_VALUE, pixelWidthHeightRatio, null); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java index c19bc9d14e..27eb2a1bb4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java @@ -56,9 +56,10 @@ import com.google.android.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); - output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, - null)); + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId()); + output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_ID3, + null, Format.NO_VALUE, null)); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java index c67e7ad0ab..ae7edc51e4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java @@ -38,6 +38,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private final MpegAudioHeader header; private final String language; + private String formatId; private TrackOutput output; private int state; @@ -76,7 +77,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId()); } @Override @@ -176,9 +179,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; frameSize = header.frameSize; if (!hasOutputFormat) { frameDurationUs = (C.MICROS_PER_SECOND * header.samplesPerFrame) / header.sampleRate; - Format format = Format.createAudioSampleFormat(null, header.mimeType, null, Format.NO_VALUE, - MpegAudioHeader.MAX_FRAME_SIZE_BYTES, header.channels, header.sampleRate, null, null, 0, - language); + Format format = Format.createAudioSampleFormat(formatId, header.mimeType, null, + Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, header.channels, header.sampleRate, + null, null, 0, language); output.format(format); hasOutputFormat = true; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java index 6e2e42d8e2..471c585277 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java @@ -28,9 +28,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private final TrackOutput output; - public SeiReader(TrackOutput output) { + public SeiReader(TrackOutput output, String formatId) { this.output = output; - output.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, null, + output.format(Format.createTextSampleFormat(formatId, MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null)); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java index 057fa636ce..625bb70560 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java @@ -36,9 +36,10 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader { public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, TsPayloadReader.TrackIdGenerator idGenerator) { this.timestampAdjuster = timestampAdjuster; - output = extractorOutput.track(idGenerator.getNextId()); - output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35, null, - Format.NO_VALUE, null)); + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId()); + output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_SCTE35, + null, Format.NO_VALUE, null)); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 61d66afbc2..99f5d0832e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -34,7 +34,10 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; /** * Facilitates the extraction of data from the MPEG-2 TS container format. @@ -79,7 +82,7 @@ public final class TsExtractor implements Extractor { private static final int BUFFER_SIZE = TS_PACKET_SIZE * BUFFER_PACKET_COUNT; private final boolean hlsMode; - private final TimestampAdjuster timestampAdjuster; + private final List timestampAdjusters; private final ParsableByteArray tsPacketBuffer; private final ParsableBitArray tsScratch; private final SparseIntArray continuityCounters; @@ -89,18 +92,12 @@ public final class TsExtractor implements Extractor { // Accessed only by the loading thread. private ExtractorOutput output; + private int remainingPmts; private boolean tracksEnded; private TsPayloadReader id3Reader; public TsExtractor() { - this(new TimestampAdjuster(0)); - } - - /** - * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. - */ - public TsExtractor(TimestampAdjuster timestampAdjuster) { - this(timestampAdjuster, new DefaultTsPayloadReaderFactory(), false); + this(new TimestampAdjuster(0), new DefaultTsPayloadReaderFactory(), false); } /** @@ -111,7 +108,12 @@ public final class TsExtractor implements Extractor { */ public TsExtractor(TimestampAdjuster timestampAdjuster, TsPayloadReader.Factory payloadReaderFactory, boolean hlsMode) { - this.timestampAdjuster = timestampAdjuster; + if (hlsMode) { + timestampAdjusters = Collections.singletonList(timestampAdjuster); + } else { + timestampAdjusters = new ArrayList<>(); + timestampAdjusters.add(timestampAdjuster); + } this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory); this.hlsMode = hlsMode; tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE); @@ -150,7 +152,10 @@ public final class TsExtractor implements Extractor { @Override public void seek(long position, long timeUs) { - timestampAdjuster.reset(); + int timestampAdjustersCount = timestampAdjusters.size(); + for (int i = 0; i < timestampAdjustersCount; i++) { + timestampAdjusters.get(i).reset(); + } tsPacketBuffer.reset(); continuityCounters.clear(); // Elementary stream readers' state should be cleared to get consistent behaviours when seeking. @@ -307,8 +312,12 @@ public final class TsExtractor implements Extractor { } else { int pid = patScratch.readBits(13); tsPayloadReaders.put(pid, new SectionReader(new PmtReader(pid))); + remainingPmts++; } } + if (!hlsMode) { + tsPayloadReaders.remove(TS_PAT_PID); + } } } @@ -345,10 +354,21 @@ public final class TsExtractor implements Extractor { // See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment. return; } - // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12), program_number (16), - // reserved (2), version_number (5), current_next_indicator (1), // section_number (8), + // TimestampAdjuster assignment. + TimestampAdjuster timestampAdjuster; + if (hlsMode || remainingPmts == 1) { + timestampAdjuster = timestampAdjusters.get(0); + } else { + timestampAdjuster = new TimestampAdjuster(timestampAdjusters.get(0).firstSampleTimestampUs); + timestampAdjusters.add(timestampAdjuster); + } + + // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12) + sectionData.skipBytes(2); + int programNumber = sectionData.readUnsignedShort(); + // reserved (2), version_number (5), current_next_indicator (1), section_number (8), // last_section_number (8), reserved (3), PCR_PID (13) - sectionData.skipBytes(9); + sectionData.skipBytes(5); // Read program_info_length. sectionData.readBytes(pmtScratch, 2); @@ -364,7 +384,7 @@ public final class TsExtractor implements Extractor { EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, new byte[0]); id3Reader = payloadReaderFactory.createPayloadReader(TS_STREAM_TYPE_ID3, dummyEsInfo); id3Reader.init(timestampAdjuster, output, - new TrackIdGenerator(TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE)); + new TrackIdGenerator(programNumber, TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE)); } int remainingEntriesLength = sectionData.bytesLeft(); @@ -393,7 +413,8 @@ public final class TsExtractor implements Extractor { } else { reader = payloadReaderFactory.createPayloadReader(streamType, esInfo); if (reader != null) { - reader.init(timestampAdjuster, output, new TrackIdGenerator(trackId, MAX_PID_PLUS_ONE)); + reader.init(timestampAdjuster, output, + new TrackIdGenerator(programNumber, trackId, MAX_PID_PLUS_ONE)); } } @@ -404,13 +425,17 @@ public final class TsExtractor implements Extractor { if (hlsMode) { if (!tracksEnded) { output.endTracks(); + remainingPmts = 0; + tracksEnded = true; } } else { - tsPayloadReaders.remove(TS_PAT_PID); tsPayloadReaders.remove(pid); - output.endTracks(); + remainingPmts--; + if (remainingPmts == 0) { + output.endTracks(); + tracksEnded = true; + } } - tracksEnded = true; } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java index 5785c50a7b..4169e0f3a0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -81,17 +81,63 @@ public interface TsPayloadReader { */ final class TrackIdGenerator { - private final int firstId; - private final int idIncrement; - private int generatedIdCount; + private static final int ID_UNSET = Integer.MIN_VALUE; - public TrackIdGenerator(int firstId, int idIncrement) { - this.firstId = firstId; - this.idIncrement = idIncrement; + private final String formatIdPrefix; + private final int firstTrackId; + private final int trackIdIncrement; + private int trackId; + private String formatId; + + public TrackIdGenerator(int firstTrackId, int trackIdIncrement) { + this(ID_UNSET, firstTrackId, trackIdIncrement); } - public int getNextId() { - return firstId + idIncrement * generatedIdCount++; + public TrackIdGenerator(int programNumber, int firstTrackId, int trackIdIncrement) { + this.formatIdPrefix = programNumber != ID_UNSET ? programNumber + "/" : ""; + this.firstTrackId = firstTrackId; + this.trackIdIncrement = trackIdIncrement; + trackId = ID_UNSET; + } + + /** + * Generates a new set of track and track format ids. Must be called before {@code get*} + * methods. + */ + public void generateNewId() { + trackId = trackId == ID_UNSET ? firstTrackId : trackId + trackIdIncrement; + formatId = formatIdPrefix + trackId; + } + + /** + * Returns the last generated track id. Must be called after the first {@link #generateNewId()} + * call. + * + * @return The last generated track id. + */ + public int getTrackId() { + maybeThrowUninitializedError(); + return trackId; + } + + /** + * Returns the last generated format id, with the format {@code "programNumber/trackId"}. If no + * {@code programNumber} was provided, the {@code trackId} alone is used as format id. Must be + * called after the first {@link #generateNewId()} call. + * + * @return The last generated format id, with the format {@code "programNumber/trackId"}. If no + * {@code programNumber} was provided, the {@code trackId} alone is used as + * format id. + */ + public String getFormatId() { + maybeThrowUninitializedError(); + return formatId; + } + + private void maybeThrowUninitializedError() { + if (trackId == ID_UNSET) { + throw new IllegalStateException("generateNewId() must be called before retrieving ids."); + } } } diff --git a/library/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java b/library/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java index 19c500202b..ace300c6b1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java @@ -34,7 +34,7 @@ public final class TimestampAdjuster { */ private static final long MAX_PTS_PLUS_ONE = 0x200000000L; - private final long firstSampleTimestampUs; + public final long firstSampleTimestampUs; private long timestampOffsetUs; From 74acbe04e35a029b570b2476fbee4135febf110e Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 1 Feb 2017 01:25:34 -0800 Subject: [PATCH 016/140] Pass an array of BufferProcessors to the AudioTrack. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146215966 --- .../ext/ffmpeg/FfmpegAudioRenderer.java | 19 +--- .../ext/flac/LibflacAudioRenderer.java | 19 +--- .../ext/opus/LibopusAudioRenderer.java | 33 +++---- .../android/exoplayer2/SimpleExoPlayer.java | 36 ++++--- .../android/exoplayer2/audio/AudioTrack.java | 88 +++++++++++------ .../exoplayer2/audio/BufferProcessor.java | 53 +++++++++-- .../audio/MediaCodecAudioRenderer.java | 16 +++- .../audio/ResamplingBufferProcessor.java | 95 +++++++++++-------- .../audio/SimpleDecoderAudioRenderer.java | 20 ++-- .../mediacodec/MediaCodecRenderer.java | 6 +- 10 files changed, 238 insertions(+), 147 deletions(-) diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java index 0aac601045..6c3ece68a2 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -19,8 +19,8 @@ import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.BufferProcessor; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; @@ -43,21 +43,12 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - */ - public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) { - super(eventHandler, eventListener); - } - - /** - * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers + * before they are output. */ public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities) { - super(eventHandler, eventListener, audioCapabilities); + BufferProcessor... bufferProcessors) { + super(eventHandler, eventListener, bufferProcessors); } @Override diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java index eb7206c9cf..5efaf98512 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.ext.flac; import android.os.Handler; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.BufferProcessor; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; @@ -38,21 +38,12 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - */ - public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) { - super(eventHandler, eventListener); - } - - /** - * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers + * before they are output. */ public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities) { - super(eventHandler, eventListener, audioCapabilities); + BufferProcessor... bufferProcessors) { + super(eventHandler, eventListener, bufferProcessors); } @Override diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index 1850e68229..f31f80f518 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.ext.opus; import android.os.Handler; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.BufferProcessor; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; @@ -40,35 +40,26 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers + * before they are output. */ - public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) { - super(eventHandler, eventListener); + public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, + BufferProcessor... bufferProcessors) { + super(eventHandler, eventListener, bufferProcessors); } /** * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio + * buffers before they are output. */ public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities) { - super(eventHandler, eventListener, audioCapabilities); - } - - /** - * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. - */ - public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities, DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys) { - super(eventHandler, eventListener, audioCapabilities, drmSessionManager, - playClearSamplesWithoutKeys); + DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + BufferProcessor... bufferProcessors) { + super(eventHandler, eventListener, null, drmSessionManager, playClearSamplesWithoutKeys, + bufferProcessors); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 298e528246..4547ec7e08 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -29,6 +29,7 @@ import android.view.SurfaceView; import android.view.TextureView; import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.BufferProcessor; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -624,7 +625,7 @@ public class SimpleExoPlayer implements ExoPlayer { buildVideoRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, componentListener, allowedVideoJoiningTimeMs, out); buildAudioRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, - componentListener, out); + componentListener, buildBufferProcessors(), out); buildTextRenderers(context, mainHandler, extensionRendererMode, componentListener, out); buildMetadataRenderers(context, mainHandler, extensionRendererMode, componentListener, out); buildMiscellaneousRenderers(context, mainHandler, extensionRendererMode, out); @@ -636,7 +637,7 @@ public class SimpleExoPlayer implements ExoPlayer { * @param context The {@link Context} associated with the player. * @param mainHandler A handler associated with the main thread's looper. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will - * not be used for DRM protected playbacks. + * not be used for DRM protected playbacks. * @param extensionRendererMode The extension renderer mode. * @param eventListener An event listener. * @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video renderers @@ -681,17 +682,19 @@ public class SimpleExoPlayer implements ExoPlayer { * @param context The {@link Context} associated with the player. * @param mainHandler A handler associated with the main thread's looper. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will - * not be used for DRM protected playbacks. + * not be used for DRM protected playbacks. * @param extensionRendererMode The extension renderer mode. * @param eventListener An event listener. + * @param bufferProcessors An array of {@link BufferProcessor}s which will process PCM audio + * buffers before they are output. May be empty. * @param out An array to which the built renderers should be appended. */ protected void buildAudioRenderers(Context context, Handler mainHandler, DrmSessionManager drmSessionManager, @ExtensionRendererMode int extensionRendererMode, AudioRendererEventListener eventListener, - ArrayList out) { + BufferProcessor[] bufferProcessors, ArrayList out) { out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true, - mainHandler, eventListener, AudioCapabilities.getCapabilities(context))); + mainHandler, eventListener, AudioCapabilities.getCapabilities(context), bufferProcessors)); if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { return; @@ -705,8 +708,9 @@ public class SimpleExoPlayer implements ExoPlayer { Class clazz = Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class); - Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); + AudioRendererEventListener.class, BufferProcessor[].class); + Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener, + bufferProcessors); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded LibopusAudioRenderer."); } catch (ClassNotFoundException e) { @@ -719,8 +723,9 @@ public class SimpleExoPlayer implements ExoPlayer { Class clazz = Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class); - Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); + AudioRendererEventListener.class, BufferProcessor[].class); + Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener, + bufferProcessors); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded LibflacAudioRenderer."); } catch (ClassNotFoundException e) { @@ -733,8 +738,9 @@ public class SimpleExoPlayer implements ExoPlayer { Class clazz = Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class); - Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); + AudioRendererEventListener.class, BufferProcessor[].class); + Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener, + bufferProcessors); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded FfmpegAudioRenderer."); } catch (ClassNotFoundException e) { @@ -787,6 +793,14 @@ public class SimpleExoPlayer implements ExoPlayer { // Do nothing. } + /** + * Builds an array of {@link BufferProcessor}s which will process PCM audio buffers before they + * are output. + */ + protected BufferProcessor[] buildBufferProcessors() { + return new BufferProcessor[0]; + } + // Internal methods. private void removeSurfaceCallbacks() { diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index 11c388fdab..c0ba7ad3e4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -90,6 +90,21 @@ public final class AudioTrack { } + /** + * Thrown when a failure occurs configuring the track. + */ + public static final class ConfigurationException extends Exception { + + public ConfigurationException(Throwable cause) { + super(cause); + } + + public ConfigurationException(String message) { + super(message); + } + + } + /** * Thrown when a failure occurs initializing an {@link android.media.AudioTrack}. */ @@ -254,6 +269,7 @@ public final class AudioTrack { public static boolean failOnSpuriousAudioTimestamp = false; private final AudioCapabilities audioCapabilities; + private final BufferProcessor[] bufferProcessors; private final Listener listener; private final ConditionVariable releasingConditionVariable; private final long[] playheadOffsets; @@ -267,12 +283,12 @@ public final class AudioTrack { private android.media.AudioTrack audioTrack; private int sampleRate; private int channelConfig; - @C.StreamType - private int streamType; @C.Encoding - private int inputEncoding; + private int encoding; @C.Encoding private int outputEncoding; + @C.StreamType + private int streamType; private boolean passthrough; private int pcmFrameSize; private int bufferSize; @@ -303,8 +319,6 @@ public final class AudioTrack { private byte[] preV21OutputBuffer; private int preV21OutputBufferOffset; - private BufferProcessor resampler; - private boolean playing; private int audioSessionId; private boolean tunneling; @@ -312,11 +326,18 @@ public final class AudioTrack { private long lastFeedElapsedRealtimeMs; /** - * @param audioCapabilities The current audio capabilities. + * @param audioCapabilities The audio capabilities for playback on this device. May be null if the + * default capabilities (no encoded audio passthrough support) should be assumed. + * @param bufferProcessors An array of {@link BufferProcessor}s which will process PCM audio + * buffers before they are output. May be empty. * @param listener Listener for audio track events. */ - public AudioTrack(AudioCapabilities audioCapabilities, Listener listener) { + public AudioTrack(AudioCapabilities audioCapabilities, BufferProcessor[] bufferProcessors, + Listener listener) { this.audioCapabilities = audioCapabilities; + this.bufferProcessors = new BufferProcessor[bufferProcessors.length + 1]; + this.bufferProcessors[0] = new ResamplingBufferProcessor(); + System.arraycopy(bufferProcessors, 0, this.bufferProcessors, 1, bufferProcessors.length); this.listener = listener; releasingConditionVariable = new ConditionVariable(true); if (Util.SDK_INT >= 18) { @@ -413,9 +434,23 @@ public final class AudioTrack { * {@link C#ENCODING_PCM_32BIT}. * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a * suitable buffer size automatically. + * @throws ConfigurationException If an error occurs configuring the track. */ public void configure(String mimeType, int channelCount, int sampleRate, - @C.PcmEncoding int pcmEncoding, int specifiedBufferSize) { + @C.PcmEncoding int pcmEncoding, int specifiedBufferSize) throws ConfigurationException { + boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); + @C.Encoding int encoding = passthrough ? getEncodingForMimeType(mimeType) : pcmEncoding; + if (!passthrough) { + for (BufferProcessor bufferProcessor : bufferProcessors) { + try { + bufferProcessor.configure(sampleRate, channelCount, encoding); + } catch (BufferProcessor.UnhandledFormatException e) { + throw new ConfigurationException(e); + } + encoding = bufferProcessor.getOutputEncoding(); + } + } + int channelConfig; switch (channelCount) { case 1: @@ -443,7 +478,7 @@ public final class AudioTrack { channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND; break; default: - throw new IllegalArgumentException("Unsupported channel count: " + channelCount); + throw new ConfigurationException("Unsupported channel count: " + channelCount); } // Workaround for overly strict channel configuration checks on nVidia Shield. @@ -461,25 +496,13 @@ public final class AudioTrack { } } - boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); - // Workaround for Nexus Player not reporting support for mono passthrough. // (See [Internal: b/34268671].) if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && passthrough && channelCount == 1) { channelConfig = AudioFormat.CHANNEL_OUT_STEREO; } - @C.Encoding int inputEncoding; - if (passthrough) { - inputEncoding = getEncodingForMimeType(mimeType); - } else if (pcmEncoding == C.ENCODING_PCM_8BIT || pcmEncoding == C.ENCODING_PCM_16BIT - || pcmEncoding == C.ENCODING_PCM_24BIT || pcmEncoding == C.ENCODING_PCM_32BIT) { - inputEncoding = pcmEncoding; - } else { - throw new IllegalArgumentException("Unsupported PCM encoding: " + pcmEncoding); - } - - if (isInitialized() && this.inputEncoding == inputEncoding && this.sampleRate == sampleRate + if (isInitialized() && this.encoding == encoding && this.sampleRate == sampleRate && this.channelConfig == channelConfig) { // We already have an audio track with the correct sample rate, channel config and encoding. return; @@ -487,15 +510,12 @@ public final class AudioTrack { reset(); - this.inputEncoding = inputEncoding; + this.encoding = encoding; this.passthrough = passthrough; this.sampleRate = sampleRate; this.channelConfig = channelConfig; pcmFrameSize = 2 * channelCount; // 2 bytes per 16-bit sample * number of channels. - outputEncoding = passthrough ? inputEncoding : C.ENCODING_PCM_16BIT; - - resampler = outputEncoding != inputEncoding ? new ResamplingBufferProcessor(inputEncoding) - : null; + outputEncoding = passthrough ? encoding : C.ENCODING_PCM_16BIT; if (specifiedBufferSize != 0) { bufferSize = specifiedBufferSize; @@ -684,8 +704,12 @@ public final class AudioTrack { } inputBuffer = buffer; - outputBuffer = resampler != null ? resampler.handleBuffer(inputBuffer, outputBuffer) - : inputBuffer; + if (!passthrough) { + for (BufferProcessor bufferProcessor : bufferProcessors) { + buffer = bufferProcessor.handleBuffer(buffer); + } + } + outputBuffer = buffer; if (Util.SDK_INT < 21) { int bytesRemaining = outputBuffer.remaining(); if (preV21OutputBuffer == null || preV21OutputBuffer.length < bytesRemaining) { @@ -886,6 +910,9 @@ public final class AudioTrack { framesPerEncodedSample = 0; inputBuffer = null; avSyncHeader = null; + for (BufferProcessor bufferProcessor : bufferProcessors) { + bufferProcessor.flush(); + } bytesUntilNextAvSync = 0; startMediaTimeState = START_NOT_SET; latencyUs = 0; @@ -919,6 +946,9 @@ public final class AudioTrack { public void release() { reset(); releaseKeepSessionIdAudioTrack(); + for (BufferProcessor bufferProcessor : bufferProcessors) { + bufferProcessor.release(); + } audioSessionId = C.AUDIO_SESSION_ID_UNSET; playing = false; } diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java index a10e8c05af..4f604f1a5d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java @@ -15,23 +15,60 @@ */ package com.google.android.exoplayer2.audio; +import com.google.android.exoplayer2.C; import java.nio.ByteBuffer; /** - * Interface for processors of buffers, for use with {@link AudioTrack}. + * Interface for processors of audio buffers. */ public interface BufferProcessor { /** - * Processes the data in the specified input buffer in its entirety. Populates {@code output} with - * processed data if is not {@code null} and has sufficient capacity. Otherwise a different buffer - * will be populated and returned. + * Exception thrown when a processor can't be configured for a given input format. + */ + final class UnhandledFormatException extends Exception { + + public UnhandledFormatException(int sampleRateHz, int channelCount, @C.Encoding int encoding) { + super("Unhandled format: " + sampleRateHz + " Hz, " + channelCount + " channels in encoding " + + encoding); + } + + } + + /** + * Configures this processor to take input buffers with the specified format. + * + * @param sampleRateHz The sample rate of input audio in Hz. + * @param channelCount The number of interleaved channels in input audio. + * @param encoding The encoding of input audio. + * @throws UnhandledFormatException Thrown if the specified format can't be handled as input. + */ + void configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) + throws UnhandledFormatException; + + /** + * Returns the encoding used in buffers output by this processor. + */ + @C.Encoding + int getOutputEncoding(); + + /** + * Processes the data in the specified input buffer in its entirety. * * @param input A buffer containing the input data to process. - * @param output A buffer into which the output should be written, if its capacity is sufficient. - * @return The processed output. Different to {@code output} if null was passed, or if its - * capacity was insufficient. + * @return A buffer containing the processed output. This may be the same as the input buffer if + * no processing was required. */ - ByteBuffer handleBuffer(ByteBuffer input, ByteBuffer output); + ByteBuffer handleBuffer(ByteBuffer input); + + /** + * Clears any state in preparation for receiving a new stream of buffers. + */ + void flush(); + + /** + * Releases any resources associated with this instance. + */ + void release(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index f8501c3858..dc7cdf42c8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -121,13 +121,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @param eventListener A listener of events. May be null if delivery of events is not required. * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers + * before they are output. */ public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, Handler eventHandler, - AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) { + AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, + BufferProcessor... bufferProcessors) { super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); - audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener()); + audioTrack = new AudioTrack(audioCapabilities, bufferProcessors, new AudioTrackListener()); eventDispatcher = new EventDispatcher(eventHandler, eventListener); } @@ -219,14 +222,19 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) { + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) + throws ExoPlaybackException { boolean passthrough = passthroughMediaFormat != null; String mimeType = passthrough ? passthroughMediaFormat.getString(MediaFormat.KEY_MIME) : MimeTypes.AUDIO_RAW; MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat; int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); - audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0); + try { + audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0); + } catch (AudioTrack.ConfigurationException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java index f0ea5e60c7..4495cfdbee 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java @@ -21,35 +21,47 @@ import com.google.android.exoplayer2.util.Assertions; import java.nio.ByteBuffer; /** - * A {@link BufferProcessor} that converts PCM input buffers from a specified input bit depth to - * {@link C#ENCODING_PCM_16BIT} in preparation for writing to an {@link android.media.AudioTrack}. + * A {@link BufferProcessor} that outputs buffers in {@link C#ENCODING_PCM_16BIT}. */ /* package */ final class ResamplingBufferProcessor implements BufferProcessor { @C.PcmEncoding - private final int inputEncoding; + private int encoding; + private ByteBuffer outputBuffer; - /** - * Creates a new buffer processor for resampling input in the specified encoding. - * - * @param inputEncoding The PCM encoding of input buffers. - * @throws IllegalArgumentException Thrown if the input encoding is not PCM or its bit depth is - * not 8, 24 or 32-bits. - */ - public ResamplingBufferProcessor(@C.PcmEncoding int inputEncoding) { - Assertions.checkArgument(inputEncoding == C.ENCODING_PCM_8BIT - || inputEncoding == C.ENCODING_PCM_24BIT || inputEncoding == C.ENCODING_PCM_32BIT); - this.inputEncoding = inputEncoding; + public ResamplingBufferProcessor() { + encoding = C.ENCODING_INVALID; } @Override - public ByteBuffer handleBuffer(ByteBuffer input, ByteBuffer output) { - int offset = input.position(); - int limit = input.limit(); - int size = limit - offset; + public void configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) + throws UnhandledFormatException { + if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT + && encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) { + throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + if (encoding == C.ENCODING_PCM_16BIT) { + outputBuffer = null; + } + this.encoding = encoding; + } + + @Override + public int getOutputEncoding() { + return C.ENCODING_PCM_16BIT; + } + + @Override + public ByteBuffer handleBuffer(ByteBuffer buffer) { + int position = buffer.position(); + int limit = buffer.limit(); + int size = limit - position; int resampledSize; - switch (inputEncoding) { + switch (encoding) { + case C.ENCODING_PCM_16BIT: + // No processing required. + return buffer; case C.ENCODING_PCM_8BIT: resampledSize = size * 2; break; @@ -59,7 +71,6 @@ import java.nio.ByteBuffer; case C.ENCODING_PCM_32BIT: resampledSize = size / 2; break; - case C.ENCODING_PCM_16BIT: case C.ENCODING_INVALID: case Format.NO_VALUE: default: @@ -67,34 +78,34 @@ import java.nio.ByteBuffer; throw new IllegalStateException(); } - ByteBuffer resampledBuffer = output; - if (resampledBuffer == null || resampledBuffer.capacity() < resampledSize) { - resampledBuffer = ByteBuffer.allocateDirect(resampledSize); + if (outputBuffer == null || outputBuffer.capacity() < resampledSize) { + outputBuffer = ByteBuffer.allocateDirect(resampledSize).order(buffer.order()); + } else { + Assertions.checkState(!outputBuffer.hasRemaining()); + outputBuffer.clear(); } - resampledBuffer.position(0); - resampledBuffer.limit(resampledSize); // Samples are little endian. - switch (inputEncoding) { + switch (encoding) { case C.ENCODING_PCM_8BIT: // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. - for (int i = offset; i < limit; i++) { - resampledBuffer.put((byte) 0); - resampledBuffer.put((byte) ((input.get(i) & 0xFF) - 128)); + for (int i = position; i < limit; i++) { + outputBuffer.put((byte) 0); + outputBuffer.put((byte) ((buffer.get(i) & 0xFF) - 128)); } break; case C.ENCODING_PCM_24BIT: // 24->16 bit resampling. Drop the least significant byte. - for (int i = offset; i < limit; i += 3) { - resampledBuffer.put(input.get(i + 1)); - resampledBuffer.put(input.get(i + 2)); + for (int i = position; i < limit; i += 3) { + outputBuffer.put(buffer.get(i + 1)); + outputBuffer.put(buffer.get(i + 2)); } break; case C.ENCODING_PCM_32BIT: // 32->16 bit resampling. Drop the two least significant bytes. - for (int i = offset; i < limit; i += 4) { - resampledBuffer.put(input.get(i + 2)); - resampledBuffer.put(input.get(i + 3)); + for (int i = position; i < limit; i += 4) { + outputBuffer.put(buffer.get(i + 2)); + outputBuffer.put(buffer.get(i + 3)); } break; case C.ENCODING_PCM_16BIT: @@ -105,8 +116,18 @@ import java.nio.ByteBuffer; throw new IllegalStateException(); } - resampledBuffer.position(0); - return resampledBuffer; + outputBuffer.flip(); + return outputBuffer; + } + + @Override + public void flush() { + // Do nothing. + } + + @Override + public void release() { + outputBuffer = null; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index d23ee769dd..9e75145626 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -102,10 +102,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers + * before they are output. */ public SimpleDecoderAudioRenderer(Handler eventHandler, - AudioRendererEventListener eventListener) { - this(eventHandler, eventListener, null); + AudioRendererEventListener eventListener, BufferProcessor... bufferProcessors) { + this(eventHandler, eventListener, null, null, false, bufferProcessors); } /** @@ -133,13 +135,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * begin in parallel with key acquisition. This parameter specifies whether the renderer is * permitted to play clear regions of encrypted media files before {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio + * buffers before they are output. */ public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, - DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) { + DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + BufferProcessor... bufferProcessors) { super(C.TRACK_TYPE_AUDIO); eventDispatcher = new EventDispatcher(eventHandler, eventListener); - audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener()); + audioTrack = new AudioTrack(audioCapabilities, bufferProcessors, new AudioTrackListener()); this.drmSessionManager = drmSessionManager; formatHolder = new FormatHolder(); this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; @@ -193,8 +198,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements while (drainOutputBuffer()) {} while (feedInputBuffer()) {} TraceUtil.endSection(); - } catch (AudioTrack.InitializationException | AudioTrack.WriteException - | AudioDecoderException e) { + } catch (AudioDecoderException | AudioTrack.ConfigurationException + | AudioTrack.InitializationException | AudioTrack.WriteException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } decoderCounters.ensureUpdated(); @@ -255,7 +260,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderException, - AudioTrack.InitializationException, AudioTrack.WriteException { + AudioTrack.ConfigurationException, AudioTrack.InitializationException, + AudioTrack.WriteException { if (outputBuffer == null) { outputBuffer = decoder.dequeueOutputBuffer(); if (outputBuffer == null) { diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 9be1c59baf..0330b13eb6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -779,8 +779,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * * @param codec The {@link MediaCodec} instance. * @param outputFormat The new output format. + * @throws ExoPlaybackException Thrown if an error occurs handling the new output format. */ - protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) { + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) + throws ExoPlaybackException { // Do nothing. } @@ -918,7 +920,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { /** * Processes a new output format. */ - private void processOutputFormat() { + private void processOutputFormat() throws ExoPlaybackException { MediaFormat format = codec.getOutputFormat(); if (codecNeedsAdaptationWorkaround && format.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT From 025a67cae950f4a8d11b2648b570e2e458bb90e8 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 1 Feb 2017 02:53:32 -0800 Subject: [PATCH 017/140] Enable buffering for CacheDataSink by default ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146221487 --- .../exoplayer2/upstream/cache/CacheDataSink.java | 7 ++++++- .../upstream/cache/CacheDataSinkFactory.java | 11 ++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 71397bd403..33b1ca58b0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -32,6 +32,9 @@ import java.io.OutputStream; */ public final class CacheDataSink implements DataSink { + /** Default buffer size. */ + public static final int DEFAULT_BUFFER_SIZE = 20480; + private final Cache cache; private final long maxCacheFileSize; private final int bufferSize; @@ -56,13 +59,15 @@ public final class CacheDataSink implements DataSink { } /** + * Constructs a CacheDataSink using the {@link #DEFAULT_BUFFER_SIZE}. + * * @param cache The cache into which data should be written. * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for * a {@link DataSpec} whose size exceeds this value, then the data will be fragmented into * multiple cache files. */ public CacheDataSink(Cache cache, long maxCacheFileSize) { - this(cache, maxCacheFileSize, 0); + this(cache, maxCacheFileSize, DEFAULT_BUFFER_SIZE); } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java index 0c8c006e2c..0b9ab66508 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java @@ -24,18 +24,27 @@ public final class CacheDataSinkFactory implements DataSink.Factory { private final Cache cache; private final long maxCacheFileSize; + private final int bufferSize; /** * @see CacheDataSink#CacheDataSink(Cache, long) */ public CacheDataSinkFactory(Cache cache, long maxCacheFileSize) { + this(cache, maxCacheFileSize, CacheDataSink.DEFAULT_BUFFER_SIZE); + } + + /** + * @see CacheDataSink#CacheDataSink(Cache, long, int) + */ + public CacheDataSinkFactory(Cache cache, long maxCacheFileSize, int bufferSize) { this.cache = cache; this.maxCacheFileSize = maxCacheFileSize; + this.bufferSize = bufferSize; } @Override public DataSink createDataSink() { - return new CacheDataSink(cache, maxCacheFileSize); + return new CacheDataSink(cache, maxCacheFileSize, bufferSize); } } From ee3c5f875fe4b09346084b9648f9896cbb7d5329 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 1 Feb 2017 08:00:48 -0800 Subject: [PATCH 018/140] Simplify chunk package ahead of EMSG/608 piping Issue: #2362 Issue: #2176 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146243681 --- .../drm/OfflineLicenseHelperTest.java | 7 +- .../exoplayer2/drm/OfflineLicenseHelper.java | 16 +++-- .../extractor/DefaultTrackOutput.java | 22 ++++--- .../source/chunk/ChunkExtractorWrapper.java | 59 ++++++++--------- .../source/chunk/ContainerMediaChunk.java | 22 ++----- .../source/chunk/InitializationChunk.java | 65 +------------------ .../source/chunk/SingleSampleMediaChunk.java | 3 +- .../source/dash/DefaultDashChunkSource.java | 29 +++------ .../smoothstreaming/DefaultSsChunkSource.java | 4 +- 9 files changed, 75 insertions(+), 152 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index 9eed8dfd3a..985e93404a 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.util.MimeTypes; import java.util.Arrays; import java.util.HashMap; import org.mockito.Mock; @@ -217,7 +218,11 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { } private static Representation newRepresentations(DrmInitData drmInitData) { - Format format = Format.createVideoSampleFormat("", "", "", 0, 0, 0, 0, 0, null, drmInitData); + Format format = Format.createVideoContainerFormat("id", MimeTypes.VIDEO_MP4, + MimeTypes.VIDEO_H264, "", Format.NO_VALUE, 1024, 768, Format.NO_VALUE, null, 0); + if (drmInitData != null) { + format = format.copyWithDrmInitData(drmInitData); + } return Representation.newInstance("", 0, format, "", new SingleSegmentBase()); } diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index f4a65931b6..0f979d6a4f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -210,11 +210,13 @@ public final class OfflineLicenseHelper { Representation representation = adaptationSet.representations.get(0); DrmInitData drmInitData = representation.format.drmInitData; if (drmInitData == null) { - InitializationChunk initializationChunk = loadInitializationChunk(dataSource, representation); + ChunkExtractorWrapper extractorWrapper = newWrappedExtractor(representation.format); + InitializationChunk initializationChunk = loadInitializationChunk(dataSource, representation, + extractorWrapper); if (initializationChunk == null) { return null; } - Format sampleFormat = initializationChunk.getSampleFormat(); + Format sampleFormat = extractorWrapper.getSampleFormat(); if (sampleFormat != null) { drmInitData = sampleFormat.drmInitData; } @@ -288,8 +290,9 @@ public final class OfflineLicenseHelper { return session; } - private static InitializationChunk loadInitializationChunk(final DataSource dataSource, - final Representation representation) throws IOException, InterruptedException { + private static InitializationChunk loadInitializationChunk(DataSource dataSource, + Representation representation, ChunkExtractorWrapper extractorWrapper) + throws IOException, InterruptedException { RangedUri rangedUri = representation.getInitializationUri(); if (rangedUri == null) { return null; @@ -298,7 +301,7 @@ public final class OfflineLicenseHelper { rangedUri.length, representation.getCacheKey()); InitializationChunk initializationChunk = new InitializationChunk(dataSource, dataSpec, representation.format, C.SELECTION_REASON_UNKNOWN, null /* trackSelectionData */, - newWrappedExtractor(representation.format)); + extractorWrapper); initializationChunk.load(); return initializationChunk; } @@ -308,8 +311,7 @@ public final class OfflineLicenseHelper { final boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM); final Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor(); - return new ChunkExtractorWrapper(extractor, format, false /* preferManifestDrmInitData */, - false /* resendFormatOnInit */); + return new ChunkExtractorWrapper(extractor, format, false /* preferManifestDrmInitData */); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java b/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java index b3bcd97048..460e8d33a8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java @@ -70,6 +70,8 @@ public final class DefaultTrackOutput implements TrackOutput { private Format downstreamFormat; // Accessed only by the loading thread (or the consuming thread when there is no loading thread). + private boolean pendingFormatAdjustment; + private Format lastUnadjustedFormat; private long sampleOffsetUs; private long totalBytesWritten; private Allocation lastAllocation; @@ -445,23 +447,24 @@ public final class DefaultTrackOutput implements TrackOutput { } /** - * Like {@link #format(Format)}, but with an offset that will be added to the timestamps of - * samples subsequently queued to the buffer. The offset is also used to adjust - * {@link Format#subsampleOffsetUs} for both the {@link Format} passed and those subsequently - * passed to {@link #format(Format)}. + * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples + * subsequently queued to the buffer. * - * @param format The format. * @param sampleOffsetUs The timestamp offset in microseconds. */ - public void formatWithOffset(Format format, long sampleOffsetUs) { - this.sampleOffsetUs = sampleOffsetUs; - format(format); + public void setSampleOffsetUs(long sampleOffsetUs) { + if (this.sampleOffsetUs != sampleOffsetUs) { + this.sampleOffsetUs = sampleOffsetUs; + pendingFormatAdjustment = true; + } } @Override public void format(Format format) { Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs); boolean formatChanged = infoQueue.format(adjustedFormat); + lastUnadjustedFormat = format; + pendingFormatAdjustment = false; if (upstreamFormatChangeListener != null && formatChanged) { upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat); } @@ -518,6 +521,9 @@ public final class DefaultTrackOutput implements TrackOutput { @Override public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, byte[] encryptionKey) { + if (pendingFormatAdjustment) { + format(lastUnadjustedFormat); + } if (!startWriteOperation()) { infoQueue.commitSampleTimestamp(timeUs); return; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java index 2623d31cef..51db1e2a17 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java @@ -30,33 +30,19 @@ import java.io.IOException; /** * An {@link Extractor} wrapper for loading chunks containing a single track. *

- * The wrapper allows switching of the {@link SeekMapOutput} and {@link TrackOutput} that receive - * parsed data. + * The wrapper allows switching of the {@link TrackOutput} that receives parsed data. */ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput { - /** - * Receives {@link SeekMap}s extracted by the wrapped {@link Extractor}. - */ - public interface SeekMapOutput { - - /** - * @see ExtractorOutput#seekMap(SeekMap) - */ - void seekMap(SeekMap seekMap); - - } - public final Extractor extractor; private final Format manifestFormat; private final boolean preferManifestDrmInitData; - private final boolean resendFormatOnInit; private boolean extractorInitialized; - private SeekMapOutput seekMapOutput; private TrackOutput trackOutput; - private Format sentFormat; + private SeekMap seekMap; + private Format sampleFormat; // Accessed only on the loader thread. private boolean seenTrack; @@ -68,34 +54,43 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput * sample {@link Format} output from the {@link Extractor}. * @param preferManifestDrmInitData Whether {@link DrmInitData} defined in {@code manifestFormat} * should be preferred when the sample and manifest {@link Format}s are merged. - * @param resendFormatOnInit Whether the extractor should resend the previous {@link Format} when - * it is initialized via {@link #init(SeekMapOutput, TrackOutput)}. */ public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat, - boolean preferManifestDrmInitData, boolean resendFormatOnInit) { + boolean preferManifestDrmInitData) { this.extractor = extractor; this.manifestFormat = manifestFormat; this.preferManifestDrmInitData = preferManifestDrmInitData; - this.resendFormatOnInit = resendFormatOnInit; } /** - * Initializes the extractor to output to the provided {@link SeekMapOutput} and - * {@link TrackOutput} instances, and configures it to receive data from a new chunk. + * Returns the {@link SeekMap} most recently output by the extractor, or null. + */ + public SeekMap getSeekMap() { + return seekMap; + } + + /** + * Returns the sample {@link Format} most recently output by the extractor, or null. + */ + public Format getSampleFormat() { + return sampleFormat; + } + + /** + * Initializes the extractor to output to the provided {@link TrackOutput}, and configures it to + * receive data from a new chunk. * - * @param seekMapOutput The {@link SeekMapOutput} that will receive extracted {@link SeekMap}s. * @param trackOutput The {@link TrackOutput} that will receive sample data. */ - public void init(SeekMapOutput seekMapOutput, TrackOutput trackOutput) { - this.seekMapOutput = seekMapOutput; + public void init(TrackOutput trackOutput) { this.trackOutput = trackOutput; if (!extractorInitialized) { extractor.init(this); extractorInitialized = true; } else { extractor.seek(0, 0); - if (resendFormatOnInit && sentFormat != null) { - trackOutput.format(sentFormat); + if (sampleFormat != null) { + trackOutput.format(sampleFormat); } } } @@ -117,15 +112,17 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput @Override public void seekMap(SeekMap seekMap) { - seekMapOutput.seekMap(seekMap); + this.seekMap = seekMap; } // TrackOutput implementation. @Override public void format(Format format) { - sentFormat = format.copyWithManifestFormatInfo(manifestFormat, preferManifestDrmInitData); - trackOutput.format(sentFormat); + sampleFormat = format.copyWithManifestFormatInfo(manifestFormat, preferManifestDrmInitData); + if (trackOutput != null) { + trackOutput.format(sampleFormat); + } } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java index 060e6130cf..44fd45d5ff 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java @@ -20,8 +20,6 @@ import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.DefaultTrackOutput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.SeekMapOutput; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; @@ -31,12 +29,11 @@ import java.io.IOException; /** * A {@link BaseMediaChunk} that uses an {@link Extractor} to decode sample data. */ -public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput { +public class ContainerMediaChunk extends BaseMediaChunk { private final int chunkCount; private final long sampleOffsetUs; private final ChunkExtractorWrapper extractorWrapper; - private final Format sampleFormat; private volatile int bytesLoaded; private volatile boolean loadCanceled; @@ -56,19 +53,15 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput * underlying media are being merged into a single load. * @param sampleOffsetUs An offset to add to the sample timestamps parsed by the extractor. * @param extractorWrapper A wrapped extractor to use for parsing the data. - * @param sampleFormat The {@link Format} of the samples in the chunk, if known. May be null if - * the data is known to define its own sample format. */ public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs, - int chunkIndex, int chunkCount, long sampleOffsetUs, ChunkExtractorWrapper extractorWrapper, - Format sampleFormat) { + int chunkIndex, int chunkCount, long sampleOffsetUs, ChunkExtractorWrapper extractorWrapper) { super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); this.chunkCount = chunkCount; this.sampleOffsetUs = sampleOffsetUs; this.extractorWrapper = extractorWrapper; - this.sampleFormat = sampleFormat; } @Override @@ -86,13 +79,6 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput return bytesLoaded; } - // SeekMapOutput implementation. - - @Override - public final void seekMap(SeekMap seekMap) { - // Do nothing. - } - // Loadable implementation. @Override @@ -116,8 +102,8 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput if (bytesLoaded == 0) { // Set the target to ourselves. DefaultTrackOutput trackOutput = getTrackOutput(); - trackOutput.formatWithOffset(sampleFormat, sampleOffsetUs); - extractorWrapper.init(this, trackOutput); + trackOutput.setSampleOffsetUs(sampleOffsetUs); + extractorWrapper.init(trackOutput); } // Load and decode the sample data. try { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java index c8c3389830..69474aa150 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java @@ -20,30 +20,19 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.SeekMapOutput; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** * A {@link Chunk} that uses an {@link Extractor} to decode initialization data for single track. */ -public final class InitializationChunk extends Chunk implements SeekMapOutput, - TrackOutput { +public final class InitializationChunk extends Chunk { private final ChunkExtractorWrapper extractorWrapper; - // Initialization results. Set by the loader thread and read by any thread that knows loading - // has completed. These variables do not need to be volatile, since a memory barrier must occur - // for the reading thread to know that loading has completed. - private Format sampleFormat; - private SeekMap seekMap; - private volatile int bytesLoaded; private volatile boolean loadCanceled; @@ -68,55 +57,6 @@ public final class InitializationChunk extends Chunk implements SeekMapOutput, return bytesLoaded; } - /** - * Returns a {@link Format} parsed from the chunk, or null. - *

- * Should be called after loading has completed. - */ - public Format getSampleFormat() { - return sampleFormat; - } - - /** - * Returns a {@link SeekMap} parsed from the chunk, or null. - *

- * Should be called after loading has completed. - */ - public SeekMap getSeekMap() { - return seekMap; - } - - // SeekMapOutput implementation. - - @Override - public void seekMap(SeekMap seekMap) { - this.seekMap = seekMap; - } - - // TrackOutput implementation. - - @Override - public void format(Format format) { - this.sampleFormat = format; - } - - @Override - public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) - throws IOException, InterruptedException { - throw new IllegalStateException("Unexpected sample data in initialization chunk"); - } - - @Override - public void sampleData(ParsableByteArray data, int length) { - throw new IllegalStateException("Unexpected sample data in initialization chunk"); - } - - @Override - public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey) { - throw new IllegalStateException("Unexpected sample data in initialization chunk"); - } - // Loadable implementation. @Override @@ -138,8 +78,7 @@ public final class InitializationChunk extends Chunk implements SeekMapOutput, ExtractorInput input = new DefaultExtractorInput(dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); if (bytesLoaded == 0) { - // Set the target to ourselves. - extractorWrapper.init(this, this); + extractorWrapper.init(null); } // Load and decode the initialization data. try { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java index d7be74535e..1afce6f2ee 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java @@ -88,7 +88,8 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { } ExtractorInput extractorInput = new DefaultExtractorInput(dataSource, bytesLoaded, length); DefaultTrackOutput trackOutput = getTrackOutput(); - trackOutput.formatWithOffset(sampleFormat, 0); + trackOutput.setSampleOffsetUs(0); + trackOutput.format(sampleFormat); // Load the sample data. int result = 0; while (result != C.RESULT_END_OF_INPUT) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 74d53d3e32..d264283b68 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -176,8 +176,7 @@ public class DefaultDashChunkSource implements DashChunkSource { RangedUri pendingInitializationUri = null; RangedUri pendingIndexUri = null; - Format sampleFormat = representationHolder.sampleFormat; - if (sampleFormat == null) { + if (representationHolder.extractorWrapper.getSampleFormat() == null) { pendingInitializationUri = selectedRepresentation.getInitializationUri(); } if (segmentIndex == null) { @@ -233,8 +232,8 @@ public class DefaultDashChunkSource implements DashChunkSource { int maxSegmentCount = Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1); out.chunk = newMediaChunk(representationHolder, dataSource, trackSelection.getSelectedFormat(), - trackSelection.getSelectionReason(), trackSelection.getSelectionData(), sampleFormat, - segmentNum, maxSegmentCount); + trackSelection.getSelectionReason(), trackSelection.getSelectionData(), segmentNum, + maxSegmentCount); } @Override @@ -243,15 +242,11 @@ public class DefaultDashChunkSource implements DashChunkSource { InitializationChunk initializationChunk = (InitializationChunk) chunk; RepresentationHolder representationHolder = representationHolders[trackSelection.indexOf(initializationChunk.trackFormat)]; - Format sampleFormat = initializationChunk.getSampleFormat(); - if (sampleFormat != null) { - representationHolder.setSampleFormat(sampleFormat); - } // The null check avoids overwriting an index obtained from the manifest with one obtained // from the stream. If the manifest defines an index then the stream shouldn't, but in cases // where it does we should ignore it. if (representationHolder.segmentIndex == null) { - SeekMap seekMap = initializationChunk.getSeekMap(); + SeekMap seekMap = representationHolder.extractorWrapper.getSeekMap(); if (seekMap != null) { representationHolder.segmentIndex = new DashWrappingSegmentIndex((ChunkIndex) seekMap); } @@ -318,7 +313,7 @@ public class DefaultDashChunkSource implements DashChunkSource { private static Chunk newMediaChunk(RepresentationHolder representationHolder, DataSource dataSource, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, Format sampleFormat, int firstSegmentNum, int maxSegmentCount) { + Object trackSelectionData, int firstSegmentNum, int maxSegmentCount) { Representation representation = representationHolder.representation; long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum); RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum); @@ -347,7 +342,7 @@ public class DefaultDashChunkSource implements DashChunkSource { long sampleOffsetUs = -representation.presentationTimeOffsetUs; return new ContainerMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, segmentCount, - sampleOffsetUs, representationHolder.extractorWrapper, sampleFormat); + sampleOffsetUs, representationHolder.extractorWrapper); } } @@ -359,7 +354,6 @@ public class DefaultDashChunkSource implements DashChunkSource { public Representation representation; public DashSegmentIndex segmentIndex; - public Format sampleFormat; private long periodDurationUs; private int segmentNumShift; @@ -371,11 +365,9 @@ public class DefaultDashChunkSource implements DashChunkSource { if (mimeTypeIsRawText(containerMimeType)) { extractorWrapper = null; } else { - boolean resendFormatOnInit = false; Extractor extractor; if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { extractor = new RawCcExtractor(representation.format); - resendFormatOnInit = true; } else if (mimeTypeIsWebm(containerMimeType)) { extractor = new MatroskaExtractor(); } else { @@ -383,17 +375,12 @@ public class DefaultDashChunkSource implements DashChunkSource { } // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. - extractorWrapper = new ChunkExtractorWrapper(extractor, - representation.format, true /* preferManifestDrmInitData */, - resendFormatOnInit); + extractorWrapper = new ChunkExtractorWrapper(extractor, representation.format, + true /* preferManifestDrmInitData */); } segmentIndex = representation.getIndex(); } - public void setSampleFormat(Format sampleFormat) { - this.sampleFormat = sampleFormat; - } - public void updateRepresentation(long newPeriodDurationUs, Representation newRepresentation) throws BehindLiveWindowException{ DashSegmentIndex oldIndex = representation.getIndex(); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index aa197806e2..2116d852ec 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -102,7 +102,7 @@ public class DefaultSsChunkSource implements SsChunkSource { FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, track, null); - extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format, false, false); + extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format, false); } } @@ -219,7 +219,7 @@ public class DefaultSsChunkSource implements SsChunkSource { long sampleOffsetUs = chunkStartTimeUs; return new ContainerMediaChunk(dataSource, dataSpec, format, trackSelectionReason, trackSelectionData, chunkStartTimeUs, chunkEndTimeUs, chunkIndex, 1, sampleOffsetUs, - extractorWrapper, format); + extractorWrapper); } } From 0402191ace91a783010370249ca017b634518bc0 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 1 Feb 2017 08:01:34 -0800 Subject: [PATCH 019/140] Make SeiReader injectable to H26xReaders This CL is a no-op refactor but allows defining the outputted channels through the TsPayloadReaderFactory. Issue:#2161 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146243736 --- .../extractor/ts/DefaultTsPayloadReaderFactory.java | 7 ++++--- .../android/exoplayer2/extractor/ts/H264Reader.java | 12 ++++++------ .../android/exoplayer2/extractor/ts/H265Reader.java | 13 ++++++++----- .../android/exoplayer2/extractor/ts/SeiReader.java | 13 ++++++++----- 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 31aa88d11a..c798494e42 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -74,10 +74,11 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact case TsExtractor.TS_STREAM_TYPE_H262: return new PesReader(new H262Reader()); case TsExtractor.TS_STREAM_TYPE_H264: - return isSet(FLAG_IGNORE_H264_STREAM) ? null : new PesReader( - new H264Reader(isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES), isSet(FLAG_DETECT_ACCESS_UNITS))); + return isSet(FLAG_IGNORE_H264_STREAM) ? null + : new PesReader(new H264Reader(new SeiReader(), isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES), + isSet(FLAG_DETECT_ACCESS_UNITS))); case TsExtractor.TS_STREAM_TYPE_H265: - return new PesReader(new H265Reader()); + return new PesReader(new H265Reader(new SeiReader())); case TsExtractor.TS_STREAM_TYPE_SPLICE_INFO: return isSet(FLAG_IGNORE_SPLICE_INFO_STREAM) ? null : new SectionReader(new SpliceInfoSectionReader()); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java index 0de6bdeaf9..5b9c2cdcea 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java @@ -39,6 +39,7 @@ import java.util.List; private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set private static final int NAL_UNIT_TYPE_PPS = 8; // Picture parameter set + private final SeiReader seiReader; private final boolean allowNonIdrKeyframes; private final boolean detectAccessUnits; private final NalUnitTargetBuffer sps; @@ -49,7 +50,6 @@ import java.util.List; private String formatId; private TrackOutput output; - private SeiReader seiReader; private SampleReader sampleReader; // State that should not be reset on seek. @@ -62,15 +62,17 @@ import java.util.List; private final ParsableByteArray seiWrapper; /** + * @param seiReader An SEI reader for consuming closed caption channels. * @param allowNonIdrKeyframes Whether to treat samples consisting of non-IDR I slices as * synchronization samples (key-frames). * @param detectAccessUnits Whether to split the input stream into access units (samples) based on * slice headers. Pass {@code false} if the stream contains access unit delimiters (AUDs). */ - public H264Reader(boolean allowNonIdrKeyframes, boolean detectAccessUnits) { - prefixFlags = new boolean[3]; + public H264Reader(SeiReader seiReader, boolean allowNonIdrKeyframes, boolean detectAccessUnits) { + this.seiReader = seiReader; this.allowNonIdrKeyframes = allowNonIdrKeyframes; this.detectAccessUnits = detectAccessUnits; + prefixFlags = new boolean[3]; sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128); pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128); sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128); @@ -93,9 +95,7 @@ import java.util.List; formatId = idGenerator.getFormatId(); output = extractorOutput.track(idGenerator.getTrackId()); sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits); - idGenerator.generateNewId(); - seiReader = new SeiReader(extractorOutput.track(idGenerator.getTrackId()), - idGenerator.getFormatId()); + seiReader.createTracks(extractorOutput, idGenerator); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index 0f8a7745a5..93cfe9f5cb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -44,10 +44,11 @@ import java.util.Collections; private static final int PREFIX_SEI_NUT = 39; private static final int SUFFIX_SEI_NUT = 40; + private final SeiReader seiReader; + private String formatId; private TrackOutput output; private SampleReader sampleReader; - private SeiReader seiReader; // State that should not be reset on seek. private boolean hasOutputFormat; @@ -67,7 +68,11 @@ import java.util.Collections; // Scratch variables to avoid allocations. private final ParsableByteArray seiWrapper; - public H265Reader() { + /** + * @param seiReader An SEI reader for consuming closed caption channels. + */ + public H265Reader(SeiReader seiReader) { + this.seiReader = seiReader; prefixFlags = new boolean[3]; vps = new NalUnitTargetBuffer(VPS_NUT, 128); sps = new NalUnitTargetBuffer(SPS_NUT, 128); @@ -95,9 +100,7 @@ import java.util.Collections; formatId = idGenerator.getFormatId(); output = extractorOutput.track(idGenerator.getTrackId()); sampleReader = new SampleReader(output); - idGenerator.generateNewId(); - seiReader = new SeiReader(extractorOutput.track(idGenerator.getTrackId()), - idGenerator.getFormatId()); + seiReader.createTracks(extractorOutput, idGenerator); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java index 471c585277..ced28c3b93 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java @@ -16,7 +16,9 @@ package com.google.android.exoplayer2.extractor.ts; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.text.cea.CeaUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -26,12 +28,13 @@ import com.google.android.exoplayer2.util.ParsableByteArray; */ /* package */ final class SeiReader { - private final TrackOutput output; + private TrackOutput output; - public SeiReader(TrackOutput output, String formatId) { - this.output = output; - output.format(Format.createTextSampleFormat(formatId, MimeTypes.APPLICATION_CEA608, null, - Format.NO_VALUE, 0, null, null)); + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId()); + output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), + MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null)); } public void consume(long pesTimeUs, ParsableByteArray seiBuffer) { From 537a3ab5be3fcf285b17505ac64c28b5f68c8fbb Mon Sep 17 00:00:00 2001 From: cdrolle Date: Thu, 2 Feb 2017 09:40:28 -0800 Subject: [PATCH 020/140] Fixed 2 issues with Cea708Decoder. The first issue occurs when we attempt to process a DtvCcPacket that hasn't been completely filled. In this case we attempted to extract data beyond the length of the packet, instead of dropping the packet as we should have. The other issue occurs when we encountered an invalid cc_data_pkt. In that case we were finalizing the entire DtvCcPacket, instead of just ignoring that particular cc_data_pkt as we should have. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146373074 --- .../google/android/exoplayer2/text/cea/Cea708Decoder.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java index fe97dc62a5..e04c246ea0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -211,7 +211,7 @@ public final class Cea708Decoder extends CeaDecoder { } if (!ccValid) { - finalizeCurrentPacket(); + // This byte-pair isn't valid, ignore it and continue. continue; } @@ -259,7 +259,8 @@ public final class Cea708Decoder extends CeaDecoder { if (currentDtvCcPacket.currentIndex != (currentDtvCcPacket.packetSize * 2 - 1)) { Log.w(TAG, "DtvCcPacket ended prematurely; size is " + (currentDtvCcPacket.packetSize * 2 - 1) + ", but current index is " + currentDtvCcPacket.currentIndex + " (sequence number " - + currentDtvCcPacket.sequenceNumber + ")"); + + currentDtvCcPacket.sequenceNumber + "); ignoring packet"); + return; } serviceBlockPacket.reset(currentDtvCcPacket.packetData, currentDtvCcPacket.currentIndex); From c82319332fc6acb03a041fa2faf2b1b9486ea1b7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 2 Feb 2017 11:53:28 -0800 Subject: [PATCH 021/140] Omit clipped samples when applying edits for audio tracks. Issue: #2408 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146389955 --- .../android/exoplayer2/extractor/mp4/AtomParsers.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 5288a3e6ba..87a4a62550 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -332,6 +332,9 @@ import java.util.List; return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); } + // Omit any sample at the end point of an edit for audio tracks. + boolean omitClippedSample = track.type == C.TRACK_TYPE_AUDIO; + // Count the number of samples after applying edits. int editedSampleCount = 0; int nextSampleIndex = 0; @@ -342,7 +345,8 @@ import java.util.List; long duration = Util.scaleLargeTimestamp(track.editListDurations[i], track.timescale, track.movieTimescale); int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true); - int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, true, false); + int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, omitClippedSample, + false); editedSampleCount += endIndex - startIndex; copyMetadata |= nextSampleIndex != startIndex; nextSampleIndex = endIndex; @@ -365,7 +369,7 @@ import java.util.List; long endMediaTime = mediaTime + Util.scaleLargeTimestamp(duration, track.timescale, track.movieTimescale); int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true); - int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, true, false); + int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, omitClippedSample, false); if (copyMetadata) { int count = endIndex - startIndex; System.arraycopy(offsets, startIndex, editedOffsets, sampleIndex, count); From f2d3af7deaf914957cf30f36b2e01821b609cac9 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 3 Feb 2017 05:50:22 -0800 Subject: [PATCH 022/140] Delete dead code and fix javadocs from hls ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146466389 --- .../source/hls/HlsSampleStreamWrapper.java | 16 +++--------- .../hls/playlist/HlsMasterPlaylist.java | 13 ++-------- .../hls/playlist/HlsPlaylistParser.java | 26 +++++++++---------- 3 files changed, 17 insertions(+), 38 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index a9bbddb69c..bee38c59b5 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -112,10 +112,9 @@ import java.util.LinkedList; * @param chunkSource A {@link HlsChunkSource} from which chunks to load are obtained. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param positionUs The position from which to start loading media. - * @param muxedAudioFormat If HLS master playlist indicates that the stream contains muxed audio, - * this is the audio {@link Format} as defined by the playlist. - * @param muxedCaptionFormat If HLS master playlist indicates that the stream contains muxed - * captions, this is the audio {@link Format} as defined by the playlist. + * @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the master playlist. + * @param muxedCaptionFormat Optional muxed closed caption {@link Format} as defined by the master + * playlist. * @param minLoadableRetryCount The minimum number of times that the source should retry a load * before propagating an error. * @param eventDispatcher A dispatcher to notify of events. @@ -266,15 +265,6 @@ import java.util.LinkedList; released = true; } - public long getLargestQueuedTimestampUs() { - long largestQueuedTimestampUs = Long.MIN_VALUE; - for (int i = 0; i < sampleQueues.size(); i++) { - largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, - sampleQueues.valueAt(i).getLargestQueuedTimestampUs()); - } - return largestQueuedTimestampUs; - } - public void setIsTimestampMaster(boolean isTimestampMaster) { chunkSource.setIsTimestampMaster(isTimestampMaster); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java index b7426fd03d..ab18fda2f0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java @@ -31,27 +31,18 @@ public final class HlsMasterPlaylist extends HlsPlaylist { */ public static final class HlsUrl { - public final String name; public final String url; public final Format format; - public final Format videoFormat; - public final Format audioFormat; - public final Format[] textFormats; public static HlsUrl createMediaPlaylistHlsUrl(String baseUri) { Format format = Format.createContainerFormat("0", MimeTypes.APPLICATION_M3U8, null, null, Format.NO_VALUE, 0, null); - return new HlsUrl(null, baseUri, format, null, null, null); + return new HlsUrl(baseUri, format); } - public HlsUrl(String name, String url, Format format, Format videoFormat, Format audioFormat, - Format[] textFormats) { - this.name = name; + public HlsUrl(String url, Format format) { this.url = url; this.format = format; - this.videoFormat = videoFormat; - this.audioFormat = audioFormat; - this.textFormats = textFormats; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index a211417501..6efd1fecb2 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -179,30 +179,28 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Fri, 3 Feb 2017 06:45:17 -0800 Subject: [PATCH 023/140] Fix FLAC extension native part compilation issues Issue: #2352 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146469547 --- extensions/flac/src/main/jni/Android.mk | 2 +- extensions/flac/src/main/jni/flac_parser.cc | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/extensions/flac/src/main/jni/Android.mk b/extensions/flac/src/main/jni/Android.mk index e009333633..ff54c1b3c0 100644 --- a/extensions/flac/src/main/jni/Android.mk +++ b/extensions/flac/src/main/jni/Android.mk @@ -31,7 +31,7 @@ LOCAL_C_INCLUDES := \ LOCAL_SRC_FILES := $(FLAC_SOURCES) LOCAL_CFLAGS += '-DVERSION="1.3.1"' -DFLAC__NO_MD5 -DFLAC__INTEGER_ONLY_LIBRARY -DFLAC__NO_ASM -LOCAL_CFLAGS += -D_REENTRANT -DPIC -DU_COMMON_IMPLEMENTATION -fPIC +LOCAL_CFLAGS += -D_REENTRANT -DPIC -DU_COMMON_IMPLEMENTATION -fPIC -DHAVE_SYS_PARAM_H LOCAL_CFLAGS += -O3 -funroll-loops -finline-functions LOCAL_LDLIBS := -llog -lz -lm diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index 7d22c7fe79..e4925cb462 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -453,7 +453,8 @@ int64_t FLACParser::getSeekPosition(int64_t timeUs) { } FLAC__StreamMetadata_SeekPoint* points = mSeekTable->points; - for (unsigned i = mSeekTable->num_points - 1; i >= 0; i--) { + for (unsigned i = mSeekTable->num_points; i > 0; ) { + i--; if (points[i].sample_number <= sample) { return firstFrameOffset + points[i].stream_offset; } From d3f4da749c8313b78d0526cacdf4ad593a774506 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 3 Feb 2017 07:06:58 -0800 Subject: [PATCH 024/140] Propagate track type through ExtractorOutput.track This allows binding by track type in ChunkExtractorWrapper, which allows the EMSG and 608 tracks to be enabled on FragmentedMp4Extractor in DefaultDashChunkSource. ChunkExtractorWrapper currently binds these to DummyTrackOutputs. Note: I wanted to pass the mimeType instead, since it's a more specific, but unfortunately there's at least one place where it's not known at the point of invoking track() (FlvExtractor). Issue #2176 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146471082 --- .../exoplayer2/ext/flac/FlacExtractor.java | 2 +- .../extractor/ts/AdtsReaderTest.java | 4 +-- .../extractor/ts/TsExtractorTest.java | 3 ++- .../exoplayer2/drm/OfflineLicenseHelper.java | 8 +++--- .../exoplayer2/extractor/ExtractorOutput.java | 11 ++++---- .../extractor/flv/FlvExtractor.java | 7 +++-- .../extractor/mkv/MatroskaExtractor.java | 7 ++++- .../extractor/mp3/Mp3Extractor.java | 2 +- .../extractor/mp4/FragmentedMp4Extractor.java | 27 ++++++++++++------- .../extractor/mp4/Mp4Extractor.java | 3 ++- .../extractor/ogg/OggExtractor.java | 3 ++- .../extractor/rawcc/RawCcExtractor.java | 2 +- .../exoplayer2/extractor/ts/Ac3Reader.java | 2 +- .../exoplayer2/extractor/ts/AdtsReader.java | 4 +-- .../exoplayer2/extractor/ts/DtsReader.java | 2 +- .../exoplayer2/extractor/ts/H262Reader.java | 2 +- .../exoplayer2/extractor/ts/H264Reader.java | 2 +- .../exoplayer2/extractor/ts/H265Reader.java | 2 +- .../exoplayer2/extractor/ts/Id3Reader.java | 2 +- .../extractor/ts/MpegAudioReader.java | 2 +- .../exoplayer2/extractor/ts/SeiReader.java | 3 ++- .../extractor/ts/SpliceInfoSectionReader.java | 2 +- .../extractor/wav/WavExtractor.java | 2 +- .../source/ExtractorMediaPeriod.java | 2 +- .../source/chunk/ChunkExtractorWrapper.java | 12 +++++++-- .../source/dash/DefaultDashChunkSource.java | 23 ++++++++++------ .../source/hls/HlsSampleStreamWrapper.java | 4 +-- .../source/hls/WebvttExtractor.java | 2 +- .../smoothstreaming/DefaultSsChunkSource.java | 5 ++-- .../testutil/FakeExtractorOutput.java | 6 ++--- 30 files changed, 98 insertions(+), 60 deletions(-) diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 42c5908619..d13194793e 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -67,7 +67,7 @@ public final class FlacExtractor implements Extractor { @Override public void init(ExtractorOutput output) { extractorOutput = output; - trackOutput = extractorOutput.track(0); + trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO); extractorOutput.endTracks(); try { decoderJni = new FlacDecoderJni(); diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java index ebb547810b..bcfa90a565 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java @@ -69,8 +69,8 @@ public class AdtsReaderTest extends TestCase { @Override protected void setUp() throws Exception { FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); - adtsOutput = fakeExtractorOutput.track(0); - id3Output = fakeExtractorOutput.track(1); + adtsOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_AUDIO); + id3Output = fakeExtractorOutput.track(1, C.TRACK_TYPE_METADATA); adtsReader = new AdtsReader(true); TrackIdGenerator idGenerator = new TrackIdGenerator(0, 1); adtsReader.createTracks(fakeExtractorOutput, idGenerator); diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index 74e0748119..9bcb1c2377 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.test.InstrumentationTestCase; import android.util.SparseArray; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -179,7 +180,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { idGenerator.generateNewId(); - output = extractorOutput.track(idGenerator.getTrackId()); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_UNKNOWN); output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), "mime", null, 0, 0, language, null, 0)); } diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 0f979d6a4f..8d057230ca 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -210,7 +210,8 @@ public final class OfflineLicenseHelper { Representation representation = adaptationSet.representations.get(0); DrmInitData drmInitData = representation.format.drmInitData; if (drmInitData == null) { - ChunkExtractorWrapper extractorWrapper = newWrappedExtractor(representation.format); + ChunkExtractorWrapper extractorWrapper = newWrappedExtractor(representation.format, + adaptationSet.type); InitializationChunk initializationChunk = loadInitializationChunk(dataSource, representation, extractorWrapper); if (initializationChunk == null) { @@ -306,12 +307,13 @@ public final class OfflineLicenseHelper { return initializationChunk; } - private static ChunkExtractorWrapper newWrappedExtractor(final Format format) { + private static ChunkExtractorWrapper newWrappedExtractor(Format format, int trackType) { final String mimeType = format.containerMimeType; final boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM); final Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor(); - return new ChunkExtractorWrapper(extractor, format, false /* preferManifestDrmInitData */); + return new ChunkExtractorWrapper(extractor, format, trackType, + false /* preferManifestDrmInitData */); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java index a547f745ca..a59cb1d1f2 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java @@ -23,17 +23,18 @@ public interface ExtractorOutput { /** * Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track. *

- * The same {@link TrackOutput} is returned if multiple calls are made with the same - * {@code trackId}. + * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}. * - * @param trackId A track identifier. + * @param id A track identifier. + * @param type The type of the track. Typically one of the {@link com.google.android.exoplayer2.C} + * {@code TRACK_TYPE_*} constants. * @return The {@link TrackOutput} for the given track identifier. */ - TrackOutput track(int trackId); + TrackOutput track(int id, int type); /** * Called when all tracks have been identified, meaning no new {@code trackId} values will be - * passed to {@link #track(int)}. + * passed to {@link #track(int, int)}. */ void endTracks(); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java index 5b396749ac..218e6ffd82 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.flv; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -183,10 +184,12 @@ public final class FlvExtractor implements Extractor, SeekMap { boolean hasAudio = (flags & 0x04) != 0; boolean hasVideo = (flags & 0x01) != 0; if (hasAudio && audioReader == null) { - audioReader = new AudioTagPayloadReader(extractorOutput.track(TAG_TYPE_AUDIO)); + audioReader = new AudioTagPayloadReader( + extractorOutput.track(TAG_TYPE_AUDIO, C.TRACK_TYPE_AUDIO)); } if (hasVideo && videoReader == null) { - videoReader = new VideoTagPayloadReader(extractorOutput.track(TAG_TYPE_VIDEO)); + videoReader = new VideoTagPayloadReader( + extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO)); } if (metadataReader == null) { metadataReader = new ScriptTagPayloadReader(null); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 970335e9d2..ed1a86e651 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -1462,6 +1462,7 @@ public final class MatroskaExtractor implements Extractor { throw new ParserException("Unrecognized codec identifier."); } + int type; Format format; @C.SelectionFlags int selectionFlags = 0; selectionFlags |= flagDefault ? C.SELECTION_FLAG_DEFAULT : 0; @@ -1469,10 +1470,12 @@ public final class MatroskaExtractor implements Extractor { // TODO: Consider reading the name elements of the tracks and, if present, incorporating them // into the trackId passed when creating the formats. if (MimeTypes.isAudio(mimeType)) { + type = C.TRACK_TYPE_AUDIO; format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, maxInputSize, channelCount, sampleRate, pcmEncoding, initializationData, drmInitData, selectionFlags, language); } else if (MimeTypes.isVideo(mimeType)) { + type = C.TRACK_TYPE_VIDEO; if (displayUnit == Track.DISPLAY_UNIT_PIXELS) { displayWidth = displayWidth == Format.NO_VALUE ? width : displayWidth; displayHeight = displayHeight == Format.NO_VALUE ? height : displayHeight; @@ -1485,17 +1488,19 @@ public final class MatroskaExtractor implements Extractor { Format.NO_VALUE, maxInputSize, width, height, Format.NO_VALUE, initializationData, Format.NO_VALUE, pixelWidthHeightRatio, projectionData, stereoMode, drmInitData); } else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) { + type = C.TRACK_TYPE_TEXT; format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, selectionFlags, language, drmInitData); } else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType) || MimeTypes.APPLICATION_PGS.equals(mimeType)) { + type = C.TRACK_TYPE_TEXT; format = Format.createImageSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, initializationData, language, drmInitData); } else { throw new ParserException("Unexpected MIME type."); } - this.output = output.track(number); + this.output = output.track(number, type); this.output.format(format); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 9bdefeceaf..ff84c7da25 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -118,7 +118,7 @@ public final class Mp3Extractor implements Extractor { @Override public void init(ExtractorOutput output) { extractorOutput = output; - trackOutput = extractorOutput.track(0); + trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO); extractorOutput.endTracks(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index f7cc42c48f..d72eb62509 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -164,7 +164,14 @@ public final class FragmentedMp4Extractor implements Extractor { private boolean haveOutputSeekMap; public FragmentedMp4Extractor() { - this(0, null); + this(0); + } + + /** + * @param flags Flags that control the extractor's behavior. + */ + public FragmentedMp4Extractor(@Flags int flags) { + this(flags, null); } /** @@ -172,20 +179,20 @@ public final class FragmentedMp4Extractor implements Extractor { * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. */ public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster) { - this(flags, null, timestampAdjuster); + this(flags, timestampAdjuster, null); } /** * @param flags Flags that control the extractor's behavior. + * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param sideloadedTrack Sideloaded track information, in the case that the extractor * will not receive a moov box in the input data. - * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. */ - public FragmentedMp4Extractor(@Flags int flags, Track sideloadedTrack, - TimestampAdjuster timestampAdjuster) { - this.sideloadedTrack = sideloadedTrack; + public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster, + Track sideloadedTrack) { this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); this.timestampAdjuster = timestampAdjuster; + this.sideloadedTrack = sideloadedTrack; atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalLength = new ParsableByteArray(4); @@ -209,7 +216,7 @@ public final class FragmentedMp4Extractor implements Extractor { public void init(ExtractorOutput output) { extractorOutput = output; if (sideloadedTrack != null) { - TrackBundle bundle = new TrackBundle(output.track(0)); + TrackBundle bundle = new TrackBundle(output.track(0, sideloadedTrack.type)); bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0)); trackBundles.put(0, bundle); maybeInitExtraTracks(); @@ -420,7 +427,7 @@ public final class FragmentedMp4Extractor implements Extractor { // We need to create the track bundles. for (int i = 0; i < trackCount; i++) { Track track = tracks.valueAt(i); - TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i)); + TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i, track.type)); trackBundle.init(track, defaultSampleValuesArray.get(track.id)); trackBundles.put(track.id, trackBundle); durationUs = Math.max(durationUs, track.durationUs); @@ -449,12 +456,12 @@ public final class FragmentedMp4Extractor implements Extractor { private void maybeInitExtraTracks() { if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0 && eventMessageTrackOutput == null) { - eventMessageTrackOutput = extractorOutput.track(trackBundles.size()); + eventMessageTrackOutput = extractorOutput.track(trackBundles.size(), C.TRACK_TYPE_METADATA); eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE)); } if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutput == null) { - cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1); + cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1, C.TRACK_TYPE_TEXT); cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null)); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 3759a80fd4..0c990f5747 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -344,7 +344,8 @@ public final class Mp4Extractor implements Extractor, SeekMap { continue; } - Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, extractorOutput.track(i)); + Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, + extractorOutput.track(i, track.type)); // Each sample has up to three bytes of overhead for the start code that replaces its length. // Allow ten source samples per output sample, like the platform extractor. int maxInputSize = trackSampleTable.maximumSize + 3 * 10; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java index 5f41126737..cc3c5de311 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -75,7 +76,7 @@ public class OggExtractor implements Extractor { @Override public void init(ExtractorOutput output) { - TrackOutput trackOutput = output.track(0); + TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); output.endTracks(); // TODO: fix the case if sniff() isn't called streamReader.init(output, trackOutput); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java index f9957aebe5..7840eafce6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java @@ -65,7 +65,7 @@ public final class RawCcExtractor implements Extractor { @Override public void init(ExtractorOutput output) { output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); - trackOutput = output.track(0); + trackOutput = output.track(0, C.TRACK_TYPE_TEXT); output.endTracks(); trackOutput.format(format); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java index afef154ed4..790c036f1d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java @@ -87,7 +87,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { generator.generateNewId(); trackFormatId = generator.getFormatId(); - output = extractorOutput.track(generator.getTrackId()); + output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java index 56793119e4..58318ea78d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java @@ -111,10 +111,10 @@ import java.util.Collections; public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { idGenerator.generateNewId(); formatId = idGenerator.getFormatId(); - output = extractorOutput.track(idGenerator.getTrackId()); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); if (exposeId3) { idGenerator.generateNewId(); - id3Output = extractorOutput.track(idGenerator.getTrackId()); + id3Output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); id3Output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, null)); } else { diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java index 50be258ae5..874de83b68 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java @@ -82,7 +82,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { idGenerator.generateNewId(); formatId = idGenerator.getFormatId(); - output = extractorOutput.track(idGenerator.getTrackId()); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index df6ba208c3..ba515d31ed 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -81,7 +81,7 @@ import java.util.Collections; public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { idGenerator.generateNewId(); formatId = idGenerator.getFormatId(); - output = extractorOutput.track(idGenerator.getTrackId()); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java index 5b9c2cdcea..c1d24b7a33 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java @@ -93,7 +93,7 @@ import java.util.List; public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { idGenerator.generateNewId(); formatId = idGenerator.getFormatId(); - output = extractorOutput.track(idGenerator.getTrackId()); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits); seiReader.createTracks(extractorOutput, idGenerator); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index 93cfe9f5cb..30a5bdc1fd 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -98,7 +98,7 @@ import java.util.Collections; public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { idGenerator.generateNewId(); formatId = idGenerator.getFormatId(); - output = extractorOutput.track(idGenerator.getTrackId()); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); sampleReader = new SampleReader(output); seiReader.createTracks(extractorOutput, idGenerator); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java index 27eb2a1bb4..7d2ecc4e74 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java @@ -57,7 +57,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { idGenerator.generateNewId(); - output = extractorOutput.track(idGenerator.getTrackId()); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, null)); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java index ae7edc51e4..6301716286 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java @@ -79,7 +79,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { idGenerator.generateNewId(); formatId = idGenerator.getFormatId(); - output = extractorOutput.track(idGenerator.getTrackId()); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java index ced28c3b93..a3f4deffcb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ts; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; @@ -32,7 +33,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { idGenerator.generateNewId(); - output = extractorOutput.track(idGenerator.getTrackId()); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null)); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java index 625bb70560..27838d4c25 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java @@ -37,7 +37,7 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader { TsPayloadReader.TrackIdGenerator idGenerator) { this.timestampAdjuster = timestampAdjuster; idGenerator.generateNewId(); - output = extractorOutput.track(idGenerator.getTrackId()); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_SCTE35, null, Format.NO_VALUE, null)); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index 3d9f8166ab..cb46aa5519 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -60,7 +60,7 @@ public final class WavExtractor implements Extractor, SeekMap { @Override public void init(ExtractorOutput output) { extractorOutput = output; - trackOutput = output.track(0); + trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); wavHeader = null; output.endTracks(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 5226043593..dc189058a6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -381,7 +381,7 @@ import java.io.IOException; // ExtractorOutput implementation. Called by the loading thread. @Override - public TrackOutput track(int id) { + public TrackOutput track(int id, int type) { DefaultTrackOutput trackOutput = sampleQueues.get(id); if (trackOutput == null) { trackOutput = new DefaultTrackOutput(allocator); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java index 51db1e2a17..4984ed0ff0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.chunk; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -37,6 +38,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput public final Extractor extractor; private final Format manifestFormat; + private final int primaryTrackType; private final boolean preferManifestDrmInitData; private boolean extractorInitialized; @@ -52,13 +54,16 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput * @param extractor The extractor to wrap. * @param manifestFormat A manifest defined {@link Format} whose data should be merged into any * sample {@link Format} output from the {@link Extractor}. + * @param primaryTrackType The type of the primary track. Typically one of the {@link C} + * {@code TRACK_TYPE_*} constants. * @param preferManifestDrmInitData Whether {@link DrmInitData} defined in {@code manifestFormat} * should be preferred when the sample and manifest {@link Format}s are merged. */ - public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat, + public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat, int primaryTrackType, boolean preferManifestDrmInitData) { this.extractor = extractor; this.manifestFormat = manifestFormat; + this.primaryTrackType = primaryTrackType; this.preferManifestDrmInitData = preferManifestDrmInitData; } @@ -98,7 +103,10 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput // ExtractorOutput implementation. @Override - public TrackOutput track(int id) { + public TrackOutput track(int id, int type) { + if (primaryTrackType != C.TRACK_TYPE_UNKNOWN && primaryTrackType != type) { + return new DummyTrackOutput(); + } Assertions.checkState(!seenTrack || seenTrackId == id); seenTrack = true; seenTrackId = id; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index d264283b68..88dcdd50be 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk; import com.google.android.exoplayer2.source.chunk.InitializationChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk; +import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.RangedUri; import com.google.android.exoplayer2.source.dash.manifest.Representation; @@ -119,11 +120,13 @@ public class DefaultDashChunkSource implements DashChunkSource { this.maxSegmentsPerLoad = maxSegmentsPerLoad; long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); - List representations = getRepresentations(); + AdaptationSet adaptationSet = getAdaptationSet(); + List representations = adaptationSet.representations; representationHolders = new RepresentationHolder[trackSelection.length()]; for (int i = 0; i < representationHolders.length; i++) { Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); - representationHolders[i] = new RepresentationHolder(periodDurationUs, representation); + representationHolders[i] = new RepresentationHolder(periodDurationUs, representation, + adaptationSet.type); } } @@ -133,7 +136,7 @@ public class DefaultDashChunkSource implements DashChunkSource { manifest = newManifest; periodIndex = newPeriodIndex; long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); - List representations = getRepresentations(); + List representations = getAdaptationSet().representations; for (int i = 0; i < representationHolders.length; i++) { Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); representationHolders[i].updateRepresentation(periodDurationUs, representation); @@ -278,8 +281,8 @@ public class DefaultDashChunkSource implements DashChunkSource { // Private methods. - private List getRepresentations() { - return manifest.getPeriod(periodIndex).adaptationSets.get(adaptationSetIndex).representations; + private AdaptationSet getAdaptationSet() { + return manifest.getPeriod(periodIndex).adaptationSets.get(adaptationSetIndex); } private long getNowUnixTimeUs() { @@ -350,6 +353,7 @@ public class DefaultDashChunkSource implements DashChunkSource { protected static final class RepresentationHolder { + public final int trackType; public final ChunkExtractorWrapper extractorWrapper; public Representation representation; @@ -358,9 +362,11 @@ public class DefaultDashChunkSource implements DashChunkSource { private long periodDurationUs; private int segmentNumShift; - public RepresentationHolder(long periodDurationUs, Representation representation) { + public RepresentationHolder(long periodDurationUs, Representation representation, + int trackType) { this.periodDurationUs = periodDurationUs; this.representation = representation; + this.trackType = trackType; String containerMimeType = representation.format.containerMimeType; if (mimeTypeIsRawText(containerMimeType)) { extractorWrapper = null; @@ -371,12 +377,13 @@ public class DefaultDashChunkSource implements DashChunkSource { } else if (mimeTypeIsWebm(containerMimeType)) { extractor = new MatroskaExtractor(); } else { - extractor = new FragmentedMp4Extractor(); + extractor = new FragmentedMp4Extractor(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK + | FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK); } // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. extractorWrapper = new ChunkExtractorWrapper(extractor, representation.format, - true /* preferManifestDrmInitData */); + trackType, true /* preferManifestDrmInitData */); } segmentIndex = representation.getIndex(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index bee38c59b5..538acbeabf 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -156,7 +156,7 @@ import java.util.LinkedList; * prepare. */ public void prepareSingleTrack(Format format) { - track(0).format(format); + track(0, C.TRACK_TYPE_UNKNOWN).format(format); sampleQueuesBuilt = true; maybeFinishPrepare(); } @@ -456,7 +456,7 @@ import java.util.LinkedList; // ExtractorOutput implementation. Called by the loading thread. @Override - public DefaultTrackOutput track(int id) { + public DefaultTrackOutput track(int id, int type) { if (sampleQueues.indexOfKey(id) >= 0) { return sampleQueues.get(id); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java index c8928ce65d..12ea2c16c7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java @@ -167,7 +167,7 @@ import java.util.regex.Pattern; } private TrackOutput buildTrackOutput(long subsampleOffsetUs) { - TrackOutput trackOutput = output.track(0); + TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_TEXT); trackOutput.format(Format.createTextSampleFormat(null, MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, language, null, subsampleOffsetUs)); output.endTracks(); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index 2116d852ec..b0a583e8e5 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -101,8 +101,9 @@ public class DefaultSsChunkSource implements SsChunkSource { trackEncryptionBoxes, nalUnitLengthFieldLength, null, null); FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME - | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, track, null); - extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format, false); + | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, null, track); + extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format, streamElement.type, + false); } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java index 3716c6d37f..ee8927ea21 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java @@ -47,13 +47,13 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab } @Override - public FakeTrackOutput track(int trackId) { - FakeTrackOutput output = trackOutputs.get(trackId); + public FakeTrackOutput track(int id, int type) { + FakeTrackOutput output = trackOutputs.get(id); if (output == null) { Assert.assertFalse(tracksEnded); numberOfTracks++; output = new FakeTrackOutput(); - trackOutputs.put(trackId, output); + trackOutputs.put(id, output); } return output; } From 7c8a3d006d7192bc69084972396327409c28ee94 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 3 Feb 2017 09:01:31 -0800 Subject: [PATCH 025/140] Flexibilize mp4 extensions detection for HLS chunks This CL adds support mp4 extensions of the form .m4_. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146479870 --- .../google/android/exoplayer2/source/hls/HlsMediaChunk.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 924d3d3ece..a3e3559724 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -56,6 +56,7 @@ import java.util.concurrent.atomic.AtomicInteger; private static final String EC3_FILE_EXTENSION = ".ec3"; private static final String MP3_FILE_EXTENSION = ".mp3"; private static final String MP4_FILE_EXTENSION = ".mp4"; + private static final String M4_FILE_EXTENSION_PREFIX = ".m4"; private static final String VTT_FILE_EXTENSION = ".vtt"; private static final String WEBVTT_FILE_EXTENSION = ".webvtt"; @@ -341,7 +342,8 @@ import java.util.concurrent.atomic.AtomicInteger; // Only reuse TS and fMP4 extractors. usingNewExtractor = false; extractor = previousExtractor; - } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION)) { + } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION) + || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) { extractor = new FragmentedMp4Extractor(0, timestampAdjuster); } else { // MPEG-2 TS segments, but we need a new extractor. From de46ed7fb90f9ec421c77e5888bb8ae4572bff83 Mon Sep 17 00:00:00 2001 From: cdrolle Date: Tue, 7 Feb 2017 07:15:39 -0800 Subject: [PATCH 026/140] Fixed an issue with Cea608Decoder in which tab commands were not being used correctly. Tab commands were being used cumulatively (i.e. moving the cursor farther and farther over) resulting in the text eventually trying to write beyond the bounds of the screen and throwing an exception. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146783284 --- .../google/android/exoplayer2/text/cea/Cea608Decoder.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index 7324c94288..261f9d0e3e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -364,7 +364,7 @@ public final class Cea608Decoder extends CeaDecoder { } else if (isPreambleAddressCode(cc1, cc2)) { handlePreambleAddressCode(cc1, cc2); } else if (isTabCtrlCode(cc1, cc2)) { - currentCueBuilder.tab(cc2 - 0x20); + currentCueBuilder.setTab(cc2 - 0x20); } else if (isMiscCode(cc1, cc2)) { handleMiscCode(cc2); } @@ -646,8 +646,8 @@ public final class Cea608Decoder extends CeaDecoder { this.indent = indent; } - public void tab(int tabs) { - tabOffset += tabs; + public void setTab(int tabs) { + tabOffset = tabs; } public void setPreambleStyle(CharacterStyle style) { From ef41303a04077dd323014ec3295950490a4ba635 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 8 Feb 2017 06:57:39 -0800 Subject: [PATCH 027/140] Keep FlacStreamInfo unobfuscated as it is accessed from native methods Issue: #2427 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146905629 --- extensions/flac/proguard-rules.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/flac/proguard-rules.txt b/extensions/flac/proguard-rules.txt index 8e7f5e17d5..ee0a9fa5b5 100644 --- a/extensions/flac/proguard-rules.txt +++ b/extensions/flac/proguard-rules.txt @@ -5,7 +5,10 @@ native ; } -# Some members of this class are being accessed from native methods. Keep them unobfuscated. +# Some members of these classes are being accessed from native methods. Keep them unobfuscated. -keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni { *; } +-keep class com.google.android.exoplayer2.util.FlacStreamInfo { + *; +} From 9e07cf7c73ad70ec9d5c2fea56c58ef8871d2142 Mon Sep 17 00:00:00 2001 From: maxjulian Date: Thu, 9 Feb 2017 11:41:20 -0800 Subject: [PATCH 028/140] Update exoplayer 1 and 2 to support stereo mesh layout. Reference spec: https://github.com/google/spatial-media/blob/master/docs/spherical-video-v2-rfc.md#semantics ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147060701 --- .../main/java/com/google/android/exoplayer2/C.java | 13 ++++++++++++- .../java/com/google/android/exoplayer2/Format.java | 2 +- .../exoplayer2/extractor/mkv/MatroskaExtractor.java | 3 +++ .../exoplayer2/extractor/mp4/AtomParsers.java | 3 +++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/C.java b/library/src/main/java/com/google/android/exoplayer2/C.java index 0b1c33bfc9..7e9fe46c10 100644 --- a/library/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/src/main/java/com/google/android/exoplayer2/C.java @@ -515,7 +515,13 @@ public final class C { * The stereo mode for 360/3D/VR videos. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({Format.NO_VALUE, STEREO_MODE_MONO, STEREO_MODE_TOP_BOTTOM, STEREO_MODE_LEFT_RIGHT}) + @IntDef({ + Format.NO_VALUE, + STEREO_MODE_MONO, + STEREO_MODE_TOP_BOTTOM, + STEREO_MODE_LEFT_RIGHT, + STEREO_MODE_STEREO_MESH + }) public @interface StereoMode {} /** * Indicates Monoscopic stereo layout, used with 360/3D/VR videos. @@ -529,6 +535,11 @@ public final class C { * Indicates Left-Right stereo layout, used with 360/3D/VR videos. */ public static final int STEREO_MODE_LEFT_RIGHT = 2; + /** + * Indicates a stereo layout where the left and right eyes have separate meshes, + * used with 360/3D/VR videos. + */ + public static final int STEREO_MODE_STEREO_MESH = 3; /** * Converts a time in microseconds to the corresponding time in milliseconds, preserving diff --git a/library/src/main/java/com/google/android/exoplayer2/Format.java b/library/src/main/java/com/google/android/exoplayer2/Format.java index bf113119a6..f001feec10 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/src/main/java/com/google/android/exoplayer2/Format.java @@ -120,7 +120,7 @@ public final class Format implements Parcelable { /** * The stereo layout for 360/3D/VR video, or {@link #NO_VALUE} if not applicable. Valid stereo * modes are {@link C#STEREO_MODE_MONO}, {@link C#STEREO_MODE_TOP_BOTTOM}, {@link - * C#STEREO_MODE_LEFT_RIGHT}. + * C#STEREO_MODE_LEFT_RIGHT}, {@link C#STEREO_MODE_STEREO_MESH}. */ @C.StereoMode public final int stereoMode; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index ed1a86e651..51ce819282 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -673,6 +673,9 @@ public final class MatroskaExtractor implements Extractor { case 3: currentTrack.stereoMode = C.STEREO_MODE_TOP_BOTTOM; break; + case 15: + currentTrack.stereoMode = C.STEREO_MODE_STEREO_MESH; + break; default: break; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 87a4a62550..54141f2545 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -720,6 +720,9 @@ import java.util.List; case 2: stereoMode = C.STEREO_MODE_LEFT_RIGHT; break; + case 3: + stereoMode = C.STEREO_MODE_STEREO_MESH; + break; default: break; } From ef475eb9c9c8c1224043a61504f296c39db38778 Mon Sep 17 00:00:00 2001 From: cdrolle Date: Thu, 9 Feb 2017 12:31:16 -0800 Subject: [PATCH 029/140] Fixed potential bug in which old paint-on captions could be drawn overtop by pop-on captions as they weren't being cleared properly. This fix was taken from YouTube's Cea608Decoder ([] ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147066662 --- .../google/android/exoplayer2/text/cea/Cea608Decoder.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index 261f9d0e3e..fe9a5fbc5c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -503,11 +503,14 @@ public final class Cea608Decoder extends CeaDecoder { return; } + int oldCaptionMode = this.captionMode; this.captionMode = captionMode; + // Clear the working memory. resetCueBuilders(); - if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_UNKNOWN) { - // When switching to roll-up or unknown, we also need to clear the caption. + if (oldCaptionMode == CC_MODE_PAINT_ON || captionMode == CC_MODE_ROLL_UP + || captionMode == CC_MODE_UNKNOWN) { + // When switching from paint-on or to roll-up or unknown, we also need to clear the caption. cues = null; } } From f7fbbe993e90ffe60c6a3ec151529bd32bf8bd55 Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 10 Feb 2017 09:49:15 -0800 Subject: [PATCH 030/140] Fix ArrayIndexOutOfBoundsException while reading SEI NAL unit ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147165453 --- .../exoplayer2/extractor/mp4/FragmentedMp4Extractor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index d72eb62509..6c3b86c19b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -1087,12 +1087,12 @@ public final class FragmentedMp4Extractor implements Extractor { sampleBytesWritten += 4; sampleSize += nalUnitLengthFieldLengthDiff; if (cea608TrackOutput != null) { - byte[] nalPayloadData = nalPayload.data; // Peek the NAL unit type byte. - input.peekFully(nalPayloadData, 0, 1); - if ((nalPayloadData[0] & 0x1F) == NAL_UNIT_TYPE_SEI) { + input.peekFully(nalPayload.data, 0, 1); + if ((nalPayload.data[0] & 0x1F) == NAL_UNIT_TYPE_SEI) { // Read the whole SEI NAL unit into nalWrapper, including the NAL unit type byte. nalPayload.reset(sampleCurrentNalBytesRemaining); + byte[] nalPayloadData = nalPayload.data; input.readFully(nalPayloadData, 0, sampleCurrentNalBytesRemaining); // Write the SEI unit straight to the output. output.sampleData(nalPayload, sampleCurrentNalBytesRemaining); From 5bfad5d99b86a43fe70357b12fbe94aa185906f0 Mon Sep 17 00:00:00 2001 From: cdrolle Date: Sun, 12 Feb 2017 19:22:20 -0800 Subject: [PATCH 031/140] Added sample mime type to the track list descriptions. The addition of sample mime types can make it easier to identify tracks in the case of mixed media (e.g. CEA-608 and CEA-708 caption tracks). This change appends the mime type to the end of the track description for all media types. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147303187 --- .../exoplayer2/demo/TrackSelectionHelper.java | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java b/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java index 936cdf90f8..338544b1ed 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java @@ -301,15 +301,18 @@ import java.util.Locale; private static String buildTrackName(Format format) { String trackName; if (MimeTypes.isVideo(format.sampleMimeType)) { - trackName = joinWithSeparator(joinWithSeparator(buildResolutionString(format), - buildBitrateString(format)), buildTrackIdString(format)); + trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator( + buildResolutionString(format), buildBitrateString(format)), buildTrackIdString(format)), + buildSampleMimeTypeString(format)); } else if (MimeTypes.isAudio(format.sampleMimeType)) { - trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format), - buildAudioPropertyString(format)), buildBitrateString(format)), - buildTrackIdString(format)); + trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(joinWithSeparator( + buildLanguageString(format), buildAudioPropertyString(format)), + buildBitrateString(format)), buildTrackIdString(format)), + buildSampleMimeTypeString(format)); } else { - trackName = joinWithSeparator(joinWithSeparator(buildLanguageString(format), - buildBitrateString(format)), buildTrackIdString(format)); + trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format), + buildBitrateString(format)), buildTrackIdString(format)), + buildSampleMimeTypeString(format)); } return trackName.length() == 0 ? "unknown" : trackName; } @@ -342,4 +345,8 @@ import java.util.Locale; return format.id == null ? "" : ("id:" + format.id); } + private static String buildSampleMimeTypeString(Format format) { + return format.sampleMimeType == null ? "" : format.sampleMimeType; + } + } From 3bc320faaf2991913a838b881bf59069543e351e Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 Feb 2017 02:44:00 -0800 Subject: [PATCH 032/140] Fix misleading method names. Issue: #2414 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147325759 --- .../com/google/android/exoplayer2/ext/opus/OpusDecoder.java | 2 +- .../java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java index 6d0deb44ae..83e461d279 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java @@ -213,7 +213,7 @@ import java.util.List; SimpleOutputBuffer outputBuffer, int sampleRate); private native int opusSecureDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize, SimpleOutputBuffer outputBuffer, int sampleRate, - ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv, + ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv, int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData); private native void opusClose(long decoder); private native void opusReset(long decoder); diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java index 0d7547d125..73ec7c2f96 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java @@ -141,7 +141,7 @@ import java.nio.ByteBuffer; private native long vpxClose(long context); private native long vpxDecode(long context, ByteBuffer encoded, int length); private native long vpxSecureDecode(long context, ByteBuffer encoded, int length, - ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv, + ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv, int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData); private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer); private native int vpxGetErrorCode(long context); From 0316ab80dfbab737283c6a38b4f7dfd98d1747e1 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 Feb 2017 09:35:24 -0800 Subject: [PATCH 033/140] Fix broken Javadoc Issue: #2433 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147355544 --- .../java/com/google/android/exoplayer2/audio/AudioTrack.java | 2 +- .../google/android/exoplayer2/drm/DefaultDrmSessionManager.java | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index c0ba7ad3e4..cc3f91bc0a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -840,7 +840,7 @@ public final class AudioTrack { * audio session id has changed. Enabling tunneling requires platform API version 21 onwards. * * @param tunnelingAudioSessionId The audio session id to use. - * @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21. + * @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21. */ public void enableTunnelingV21(int tunnelingAudioSessionId) { Assertions.checkState(Util.SDK_INT >= 21); diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 1cd8d8464d..3af0f8a5c0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -280,6 +280,7 @@ public class DefaultDrmSessionManager implements DrmSe * required. * *

{@code mode} must be one of these: + *

    *
  • {@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is * requested otherwise the offline license is restored. *
  • {@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license @@ -288,6 +289,7 @@ public class DefaultDrmSessionManager implements DrmSe * requested otherwise the offline license is renewed. *
  • {@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline license * is released. + *
* * @param mode The mode to be set. * @param offlineLicenseKeySetId The key set id of the license to be used with the given mode. From 8cb3b6ed07ba95cd2f9297b9c351d02c090e9dda Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 Feb 2017 10:03:50 -0800 Subject: [PATCH 034/140] SmoothStreaming: Replace variant bitrate/start_time placeholders Issue: #2447 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147358615 --- .../smoothstreaming/manifest/SsManifest.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java index 844ffc45e6..1bb877eb59 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java @@ -128,8 +128,10 @@ public class SsManifest { */ public static class StreamElement { - private static final String URL_PLACEHOLDER_START_TIME = "{start time}"; - private static final String URL_PLACEHOLDER_BITRATE = "{bitrate}"; + private static final String URL_PLACEHOLDER_START_TIME_1 = "{start time}"; + private static final String URL_PLACEHOLDER_START_TIME_2 = "{start_time}"; + private static final String URL_PLACEHOLDER_BITRATE_1 = "{bitrate}"; + private static final String URL_PLACEHOLDER_BITRATE_2 = "{Bitrate}"; public final int type; public final String subType; @@ -216,9 +218,13 @@ public class SsManifest { Assertions.checkState(formats != null); Assertions.checkState(chunkStartTimes != null); Assertions.checkState(chunkIndex < chunkStartTimes.size()); + String bitrateString = Integer.toString(formats[track].bitrate); + String startTimeString = chunkStartTimes.get(chunkIndex).toString(); String chunkUrl = chunkTemplate - .replace(URL_PLACEHOLDER_BITRATE, Integer.toString(formats[track].bitrate)) - .replace(URL_PLACEHOLDER_START_TIME, chunkStartTimes.get(chunkIndex).toString()); + .replace(URL_PLACEHOLDER_BITRATE_1, bitrateString) + .replace(URL_PLACEHOLDER_BITRATE_2, bitrateString) + .replace(URL_PLACEHOLDER_START_TIME_1, startTimeString) + .replace(URL_PLACEHOLDER_START_TIME_2, startTimeString); return UriUtil.resolveToUri(baseUri, chunkUrl); } From 7625c1b862db7a0dc860832214a2beba79c22c17 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 Feb 2017 13:27:17 -0800 Subject: [PATCH 035/140] Remove unnecessary configuration parameter. - It's always fine to prefer the manifest drm init data. In DASH we always do this anyway. In SS the manifest and sample formats are identical. - Optimized the case where the manifest and sample formats are identical by avoiding the format copy. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147385282 --- .../java/com/google/android/exoplayer2/Format.java | 11 +++++++---- .../android/exoplayer2/drm/OfflineLicenseHelper.java | 3 +-- .../source/chunk/ChunkExtractorWrapper.java | 10 ++-------- .../source/dash/DefaultDashChunkSource.java | 3 +-- .../source/smoothstreaming/DefaultSsChunkSource.java | 3 +-- 5 files changed, 12 insertions(+), 18 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/Format.java b/library/src/main/java/com/google/android/exoplayer2/Format.java index f001feec10..866e512288 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/src/main/java/com/google/android/exoplayer2/Format.java @@ -438,16 +438,19 @@ public final class Format implements Parcelable { drmInitData, metadata); } - public Format copyWithManifestFormatInfo(Format manifestFormat, - boolean preferManifestDrmInitData) { + public Format copyWithManifestFormatInfo(Format manifestFormat) { + if (this == manifestFormat) { + // No need to copy from ourselves. + return this; + } String id = manifestFormat.id; String codecs = this.codecs == null ? manifestFormat.codecs : this.codecs; int bitrate = this.bitrate == NO_VALUE ? manifestFormat.bitrate : this.bitrate; float frameRate = this.frameRate == NO_VALUE ? manifestFormat.frameRate : this.frameRate; @C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags; String language = this.language == null ? manifestFormat.language : this.language; - DrmInitData drmInitData = (preferManifestDrmInitData && manifestFormat.drmInitData != null) - || this.drmInitData == null ? manifestFormat.drmInitData : this.drmInitData; + DrmInitData drmInitData = manifestFormat.drmInitData != null ? manifestFormat.drmInitData + : this.drmInitData; return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags, diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 8d057230ca..b3729c2377 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -312,8 +312,7 @@ public final class OfflineLicenseHelper { final boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM); final Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor(); - return new ChunkExtractorWrapper(extractor, format, trackType, - false /* preferManifestDrmInitData */); + return new ChunkExtractorWrapper(extractor, format, trackType); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java index 4984ed0ff0..489f63be2b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source.chunk; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -39,7 +38,6 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput private final Format manifestFormat; private final int primaryTrackType; - private final boolean preferManifestDrmInitData; private boolean extractorInitialized; private TrackOutput trackOutput; @@ -56,15 +54,11 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput * sample {@link Format} output from the {@link Extractor}. * @param primaryTrackType The type of the primary track. Typically one of the {@link C} * {@code TRACK_TYPE_*} constants. - * @param preferManifestDrmInitData Whether {@link DrmInitData} defined in {@code manifestFormat} - * should be preferred when the sample and manifest {@link Format}s are merged. */ - public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat, int primaryTrackType, - boolean preferManifestDrmInitData) { + public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat, int primaryTrackType) { this.extractor = extractor; this.manifestFormat = manifestFormat; this.primaryTrackType = primaryTrackType; - this.preferManifestDrmInitData = preferManifestDrmInitData; } /** @@ -127,7 +121,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput @Override public void format(Format format) { - sampleFormat = format.copyWithManifestFormatInfo(manifestFormat, preferManifestDrmInitData); + sampleFormat = format.copyWithManifestFormatInfo(manifestFormat); if (trackOutput != null) { trackOutput.format(sampleFormat); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 88dcdd50be..c553e4eb40 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -382,8 +382,7 @@ public class DefaultDashChunkSource implements DashChunkSource { } // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. - extractorWrapper = new ChunkExtractorWrapper(extractor, representation.format, - trackType, true /* preferManifestDrmInitData */); + extractorWrapper = new ChunkExtractorWrapper(extractor, representation.format, trackType); } segmentIndex = representation.getIndex(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index b0a583e8e5..e17d72ab37 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -102,8 +102,7 @@ public class DefaultSsChunkSource implements SsChunkSource { FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, null, track); - extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format, streamElement.type, - false); + extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format, streamElement.type); } } From 3691454b82506e23d0663e84becd6faa1265c772 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 14 Feb 2017 05:22:09 -0800 Subject: [PATCH 036/140] Fix resuming after error for CEA-608 SEI in fMP4. Also test SEI parsing in FragmentedMp4ExtractorTest. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147460699 --- .../assets/mp4/sample_fragmented_sei.mp4 | Bin 0 -> 106093 bytes .../mp4/sample_fragmented_sei.mp4.0.dump | 382 ++++++++++++++++++ .../mp4/FragmentedMp4ExtractorTest.java | 31 +- .../extractor/mp4/FragmentedMp4Extractor.java | 72 ++-- .../android/exoplayer2/util/NalUnitUtil.java | 17 +- 5 files changed, 453 insertions(+), 49 deletions(-) create mode 100644 library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4 create mode 100644 library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump diff --git a/library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4 b/library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..16907fdd987223e0996d3cfc65ba346cb4123752 GIT binary patch literal 106093 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 @@ -1066,49 +1066,47 @@ public final class FragmentedMp4Extractor implements Extractor { if (track.nalUnitLengthFieldLength != 0) { // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case // they're only 1 or 2 bytes long. - byte[] nalLengthData = nalLength.data; - nalLengthData[0] = 0; - nalLengthData[1] = 0; - nalLengthData[2] = 0; - int nalUnitLengthFieldLength = track.nalUnitLengthFieldLength; + byte[] nalPrefixData = nalPrefix.data; + nalPrefixData[0] = 0; + nalPrefixData[1] = 0; + nalPrefixData[2] = 0; + int nalUnitPrefixLength = track.nalUnitLengthFieldLength + 1; int nalUnitLengthFieldLengthDiff = 4 - track.nalUnitLengthFieldLength; // NAL units are length delimited, but the decoder requires start code delimited units. // Loop until we've written the sample to the track output, replacing length delimiters with // start codes as we encounter them. while (sampleBytesWritten < sampleSize) { if (sampleCurrentNalBytesRemaining == 0) { - // Read the NAL length so that we know where we find the next one. - input.readFully(nalLength.data, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); - nalLength.setPosition(0); - sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt(); + // Read the NAL length so that we know where we find the next one, and its type. + input.readFully(nalPrefixData, nalUnitLengthFieldLengthDiff, nalUnitPrefixLength); + nalPrefix.setPosition(0); + sampleCurrentNalBytesRemaining = nalPrefix.readUnsignedIntToInt() - 1; // Write a start code for the current NAL unit. nalStartCode.setPosition(0); output.sampleData(nalStartCode, 4); - sampleBytesWritten += 4; + // Write the NAL unit type byte. + output.sampleData(nalPrefix, 1); + processSeiNalUnitPayload = cea608TrackOutput != null + && NalUnitUtil.isNalUnitSei(nalPrefixData[4]); + sampleBytesWritten += 5; sampleSize += nalUnitLengthFieldLengthDiff; - if (cea608TrackOutput != null) { - // Peek the NAL unit type byte. - input.peekFully(nalPayload.data, 0, 1); - if ((nalPayload.data[0] & 0x1F) == NAL_UNIT_TYPE_SEI) { - // Read the whole SEI NAL unit into nalWrapper, including the NAL unit type byte. - nalPayload.reset(sampleCurrentNalBytesRemaining); - byte[] nalPayloadData = nalPayload.data; - input.readFully(nalPayloadData, 0, sampleCurrentNalBytesRemaining); - // Write the SEI unit straight to the output. - output.sampleData(nalPayload, sampleCurrentNalBytesRemaining); - sampleBytesWritten += sampleCurrentNalBytesRemaining; - sampleCurrentNalBytesRemaining = 0; - // Unescape and process the SEI unit. - int unescapedLength = NalUnitUtil.unescapeStream(nalPayloadData, nalPayload.limit()); - nalPayload.setPosition(1); // Skip the NAL unit type byte. - nalPayload.setLimit(unescapedLength); - CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalPayload, - cea608TrackOutput); - } - } } else { - // Write the payload of the NAL unit. - int writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false); + int writtenBytes; + if (processSeiNalUnitPayload) { + // Read and write the payload of the SEI NAL unit. + nalBuffer.reset(sampleCurrentNalBytesRemaining); + input.readFully(nalBuffer.data, 0, sampleCurrentNalBytesRemaining); + output.sampleData(nalBuffer, sampleCurrentNalBytesRemaining); + writtenBytes = sampleCurrentNalBytesRemaining; + // Unescape and process the SEI NAL unit. + int unescapedLength = NalUnitUtil.unescapeStream(nalBuffer.data, nalBuffer.limit()); + nalBuffer.reset(unescapedLength); + CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalBuffer, + cea608TrackOutput); + } else { + // Write the payload of the NAL unit. + writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false); + } sampleBytesWritten += writtenBytes; sampleCurrentNalBytesRemaining -= writtenBytes; } diff --git a/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java b/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java index a452871afc..a2643d5177 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java @@ -103,7 +103,8 @@ public final class NalUnitUtil { 2f }; - private static final int NAL_UNIT_TYPE_SPS = 7; + private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information + private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set private static final Object scratchEscapePositionsLock = new Object(); @@ -197,6 +198,17 @@ public final class NalUnitUtil { data.clear(); } + /** + * Returns whether the NAL unit with the specified header contains supplemental enhancement + * information. + * + * @param nalUnitHeader The header of the NAL unit (first byte of nal_unit()). + * @return Whether the NAL unit with the specified header is an SEI NAL unit. + */ + public static boolean isNalUnitSei(byte nalUnitHeader) { + return (nalUnitHeader & 0x1F) == NAL_UNIT_TYPE_SEI; + } + /** * Returns the type of the NAL unit in {@code data} that starts at {@code offset}. * @@ -297,7 +309,8 @@ public final class NalUnitUtil { int frameCropRightOffset = data.readUnsignedExpGolombCodedInt(); int frameCropTopOffset = data.readUnsignedExpGolombCodedInt(); int frameCropBottomOffset = data.readUnsignedExpGolombCodedInt(); - int cropUnitX, cropUnitY; + int cropUnitX; + int cropUnitY; if (chromaFormatIdc == 0) { cropUnitX = 1; cropUnitY = 2 - (frameMbsOnlyFlag ? 1 : 0); From fd6012a72722a42c78cf0742c3986feaf76a8829 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 14 Feb 2017 17:04:48 -0800 Subject: [PATCH 037/140] Remove outputBuffer assertion in ResamplingBufferProcessor. The outputBuffer is not necessarily empty after a flush, so the assertion could fail in normal usage. The assertion can just be removed as the output buffer is rewritten in full on every call to handleBuffer. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147541016 --- .../android/exoplayer2/audio/ResamplingBufferProcessor.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java index 4495cfdbee..507cdbcdd1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.audio; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.util.Assertions; import java.nio.ByteBuffer; /** @@ -81,7 +80,6 @@ import java.nio.ByteBuffer; if (outputBuffer == null || outputBuffer.capacity() < resampledSize) { outputBuffer = ByteBuffer.allocateDirect(resampledSize).order(buffer.order()); } else { - Assertions.checkState(!outputBuffer.hasRemaining()); outputBuffer.clear(); } From 5c571e6e9d24086abede757f5be5d275cd67a515 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 15 Feb 2017 09:58:44 -0800 Subject: [PATCH 038/140] Handle H.265/HEVC SEI NAL units in FragmentedMp4Extractor. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147609330 --- .../extractor/mp4/FragmentedMp4Extractor.java | 6 ++++-- .../android/exoplayer2/util/NalUnitUtil.java | 17 +++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index bc9b0fcad6..8144880338 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -1087,7 +1087,7 @@ public final class FragmentedMp4Extractor implements Extractor { // Write the NAL unit type byte. output.sampleData(nalPrefix, 1); processSeiNalUnitPayload = cea608TrackOutput != null - && NalUnitUtil.isNalUnitSei(nalPrefixData[4]); + && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]); sampleBytesWritten += 5; sampleSize += nalUnitLengthFieldLengthDiff; } else { @@ -1100,7 +1100,9 @@ public final class FragmentedMp4Extractor implements Extractor { writtenBytes = sampleCurrentNalBytesRemaining; // Unescape and process the SEI NAL unit. int unescapedLength = NalUnitUtil.unescapeStream(nalBuffer.data, nalBuffer.limit()); - nalBuffer.reset(unescapedLength); + // If the format is H.265/HEVC the NAL unit header has two bytes so skip one more byte. + nalBuffer.setPosition(MimeTypes.VIDEO_H265.equals(track.format.sampleMimeType) ? 1 : 0); + nalBuffer.setLimit(unescapedLength); CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalBuffer, cea608TrackOutput); } else { diff --git a/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java b/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java index a2643d5177..ab2fec0db7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java @@ -103,8 +103,9 @@ public final class NalUnitUtil { 2f }; - private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information - private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set + private static final int H264_NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information + private static final int H264_NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set + private static final int H265_NAL_UNIT_TYPE_PREFIX_SEI = 39; private static final Object scratchEscapePositionsLock = new Object(); @@ -177,7 +178,7 @@ public final class NalUnitUtil { while (offset + 1 < length) { int value = data.get(offset) & 0xFF; if (consecutiveZeros == 3) { - if (value == 1 && (data.get(offset + 1) & 0x1F) == NAL_UNIT_TYPE_SPS) { + if (value == 1 && (data.get(offset + 1) & 0x1F) == H264_NAL_UNIT_TYPE_SPS) { // Copy from this NAL unit onwards to the start of the buffer. ByteBuffer offsetData = data.duplicate(); offsetData.position(offset - 3); @@ -202,11 +203,15 @@ public final class NalUnitUtil { * Returns whether the NAL unit with the specified header contains supplemental enhancement * information. * - * @param nalUnitHeader The header of the NAL unit (first byte of nal_unit()). + * @param mimeType The sample MIME type. + * @param nalUnitHeaderFirstByte The first byte of nal_unit(). * @return Whether the NAL unit with the specified header is an SEI NAL unit. */ - public static boolean isNalUnitSei(byte nalUnitHeader) { - return (nalUnitHeader & 0x1F) == NAL_UNIT_TYPE_SEI; + public static boolean isNalUnitSei(String mimeType, byte nalUnitHeaderFirstByte) { + return (MimeTypes.VIDEO_H264.equals(mimeType) + && (nalUnitHeaderFirstByte & 0x1F) == H264_NAL_UNIT_TYPE_SEI) + || (MimeTypes.VIDEO_H265.equals(mimeType) + && ((nalUnitHeaderFirstByte & 0x7E) >> 1) == H265_NAL_UNIT_TYPE_PREFIX_SEI); } /** From d6e15b79538b39c6ddf976bf18a951394816819c Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 Feb 2017 10:27:26 -0800 Subject: [PATCH 039/140] DASH: Correctly handle empty segment indices Issue: #1865 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147613244 --- .../source/dash/DashMediaSource.java | 30 ++++++++----- .../source/dash/DashSegmentIndex.java | 16 +++---- .../source/dash/DashWrappingSegmentIndex.java | 4 +- .../source/dash/DefaultDashChunkSource.java | 44 ++++++++++++------- .../source/dash/manifest/Representation.java | 4 +- .../source/dash/manifest/SegmentBase.java | 40 ++++++++++------- .../dash/manifest/SingleSegmentIndex.java | 4 +- 7 files changed, 82 insertions(+), 60 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 99845c057e..eec99521f1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -572,22 +572,28 @@ public final class DashMediaSource implements MediaSource { long availableStartTimeUs = 0; long availableEndTimeUs = Long.MAX_VALUE; boolean isIndexExplicit = false; + boolean seenEmptyIndex = false; for (int i = 0; i < adaptationSetCount; i++) { DashSegmentIndex index = period.adaptationSets.get(i).representations.get(0).getIndex(); if (index == null) { return new PeriodSeekInfo(true, 0, durationUs); } - int firstSegmentNum = index.getFirstSegmentNum(); - int lastSegmentNum = index.getLastSegmentNum(durationUs); isIndexExplicit |= index.isExplicit(); - long adaptationSetAvailableStartTimeUs = index.getTimeUs(firstSegmentNum); - availableStartTimeUs = Math.max(availableStartTimeUs, adaptationSetAvailableStartTimeUs); - if (lastSegmentNum != DashSegmentIndex.INDEX_UNBOUNDED) { - long adaptationSetAvailableEndTimeUs = index.getTimeUs(lastSegmentNum) - + index.getDurationUs(lastSegmentNum, durationUs); - availableEndTimeUs = Math.min(availableEndTimeUs, adaptationSetAvailableEndTimeUs); - } else { - // The available end time is unmodified, because this index is unbounded. + int segmentCount = index.getSegmentCount(durationUs); + if (segmentCount == 0) { + seenEmptyIndex = true; + availableStartTimeUs = 0; + availableEndTimeUs = 0; + } else if (!seenEmptyIndex) { + int firstSegmentNum = index.getFirstSegmentNum(); + long adaptationSetAvailableStartTimeUs = index.getTimeUs(firstSegmentNum); + availableStartTimeUs = Math.max(availableStartTimeUs, adaptationSetAvailableStartTimeUs); + if (segmentCount != DashSegmentIndex.INDEX_UNBOUNDED) { + int lastSegmentNum = firstSegmentNum + segmentCount - 1; + long adaptationSetAvailableEndTimeUs = index.getTimeUs(lastSegmentNum) + + index.getDurationUs(lastSegmentNum, durationUs); + availableEndTimeUs = Math.min(availableEndTimeUs, adaptationSetAvailableEndTimeUs); + } } } return new PeriodSeekInfo(isIndexExplicit, availableStartTimeUs, availableEndTimeUs); @@ -704,8 +710,8 @@ public final class DashMediaSource implements MediaSource { // not correspond to the start of a segment in both, but this is an edge case. DashSegmentIndex snapIndex = period.adaptationSets.get(videoAdaptationSetIndex) .representations.get(0).getIndex(); - if (snapIndex == null) { - // Video adaptation set does not include an index for snapping. + if (snapIndex == null || snapIndex.getSegmentCount(periodDurationUs) == 0) { + // Video adaptation set does not include a non-empty index for snapping. return windowDefaultStartPositionUs; } int segmentNum = snapIndex.getSegmentNum(defaultStartPositionInPeriodUs, periodDurationUs); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java index d002831c4f..2ddc7f4f80 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java @@ -26,12 +26,10 @@ public interface DashSegmentIndex { int INDEX_UNBOUNDED = -1; /** - * Returns the segment number of the segment containing a given media time. - *

- * If the given media time is outside the range of the index, then the returned segment number is - * clamped to {@link #getFirstSegmentNum()} (if the given media time is earlier the start of the - * first segment) or {@link #getLastSegmentNum(long)} (if the given media time is later then the - * end of the last segment). + * Returns {@code getFirstSegmentNum()} if the index has no segments or if the given media time is + * earlier than the start of the first segment. Returns {@code getFirstSegmentNum() + + * getSegmentCount() - 1} if the given media time is later than the end of the last segment. + * Otherwise, returns the segment number of the segment containing the given media time. * * @param timeUs The time in microseconds. * @param periodDurationUs The duration of the enclosing period in microseconds, or @@ -74,7 +72,7 @@ public interface DashSegmentIndex { int getFirstSegmentNum(); /** - * Returns the segment number of the last segment, or {@link #INDEX_UNBOUNDED}. + * Returns the number of segments in the index, or {@link #INDEX_UNBOUNDED}. *

* An unbounded index occurs if a dynamic manifest uses SegmentTemplate elements without a * SegmentTimeline element, and if the period duration is not yet known. In this case the caller @@ -82,9 +80,9 @@ public interface DashSegmentIndex { * * @param periodDurationUs The duration of the enclosing period in microseconds, or * {@link C#TIME_UNSET} if the period's duration is not yet known. - * @return The segment number of the last segment, or {@link #INDEX_UNBOUNDED}. + * @return The number of segments in the index, or {@link #INDEX_UNBOUNDED}. */ - int getLastSegmentNum(long periodDurationUs); + int getSegmentCount(long periodDurationUs); /** * Returns true if segments are defined explicitly by the index. diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java index 56ea626120..40f3448f6a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java @@ -39,8 +39,8 @@ import com.google.android.exoplayer2.source.dash.manifest.RangedUri; } @Override - public int getLastSegmentNum(long periodDurationUs) { - return chunkIndex.length - 1; + public int getSegmentCount(long periodDurationUs) { + return chunkIndex.length; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index c553e4eb40..4548bc75f8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -194,10 +194,16 @@ public class DefaultDashChunkSource implements DashChunkSource { } long nowUs = getNowUnixTimeUs(); + int availableSegmentCount = representationHolder.getSegmentCount(); + if (availableSegmentCount == 0) { + // The index doesn't define any segments. + out.endOfStream = !manifest.dynamic || (periodIndex < manifest.getPeriodCount() - 1); + return; + } + int firstAvailableSegmentNum = representationHolder.getFirstSegmentNum(); - int lastAvailableSegmentNum = representationHolder.getLastSegmentNum(); - boolean indexUnbounded = lastAvailableSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED; - if (indexUnbounded) { + int lastAvailableSegmentNum; + if (availableSegmentCount == DashSegmentIndex.INDEX_UNBOUNDED) { // The index is itself unbounded. We need to use the current time to calculate the range of // available segments. long liveEdgeTimeUs = nowUs - manifest.availabilityStartTime * 1000; @@ -211,6 +217,8 @@ public class DefaultDashChunkSource implements DashChunkSource { // getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the // index of the last completed segment. lastAvailableSegmentNum = representationHolder.getSegmentNum(liveEdgeTimeInPeriodUs) - 1; + } else { + lastAvailableSegmentNum = firstAvailableSegmentNum + availableSegmentCount - 1; } int segmentNum; @@ -268,10 +276,13 @@ public class DefaultDashChunkSource implements DashChunkSource { && ((InvalidResponseCodeException) e).responseCode == 404) { RepresentationHolder representationHolder = representationHolders[trackSelection.indexOf(chunk.trackFormat)]; - int lastAvailableSegmentNum = representationHolder.getLastSegmentNum(); - if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) { - missingLastSegment = true; - return true; + int segmentCount = representationHolder.getSegmentCount(); + if (segmentCount != DashSegmentIndex.INDEX_UNBOUNDED && segmentCount != 0) { + int lastAvailableSegmentNum = representationHolder.getFirstSegmentNum() + segmentCount - 1; + if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) { + missingLastSegment = true; + return true; + } } } // Blacklist if appropriate. @@ -405,15 +416,20 @@ public class DefaultDashChunkSource implements DashChunkSource { return; } - int oldIndexLastSegmentNum = oldIndex.getLastSegmentNum(periodDurationUs); + int oldIndexSegmentCount = oldIndex.getSegmentCount(periodDurationUs); + if (oldIndexSegmentCount == 0) { + // Segment numbers cannot shift if the old index was empty. + return; + } + + int oldIndexLastSegmentNum = oldIndex.getFirstSegmentNum() + oldIndexSegmentCount - 1; long oldIndexEndTimeUs = oldIndex.getTimeUs(oldIndexLastSegmentNum) + oldIndex.getDurationUs(oldIndexLastSegmentNum, periodDurationUs); int newIndexFirstSegmentNum = newIndex.getFirstSegmentNum(); long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum); if (oldIndexEndTimeUs == newIndexStartTimeUs) { // The new index continues where the old one ended, with no overlap. - segmentNumShift += oldIndex.getLastSegmentNum(periodDurationUs) + 1 - - newIndexFirstSegmentNum; + segmentNumShift += oldIndexLastSegmentNum + 1 - newIndexFirstSegmentNum; } else if (oldIndexEndTimeUs < newIndexStartTimeUs) { // There's a gap between the old index and the new one which means we've slipped behind the // live window and can't proceed. @@ -429,12 +445,8 @@ public class DefaultDashChunkSource implements DashChunkSource { return segmentIndex.getFirstSegmentNum() + segmentNumShift; } - public int getLastSegmentNum() { - int lastSegmentNum = segmentIndex.getLastSegmentNum(periodDurationUs); - if (lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED) { - return DashSegmentIndex.INDEX_UNBOUNDED; - } - return lastSegmentNum + segmentNumShift; + public int getSegmentCount() { + return segmentIndex.getSegmentCount(periodDurationUs); } public long getSegmentStartTimeUs(int segmentNum) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java index 4146037e1c..5960d4d7ba 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java @@ -318,8 +318,8 @@ public abstract class Representation { } @Override - public int getLastSegmentNum(long periodDurationUs) { - return segmentBase.getLastSegmentNum(periodDurationUs); + public int getSegmentCount(long periodDurationUs) { + return segmentBase.getSegmentCount(periodDurationUs); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java index 70a65e932a..4f7dc81fc5 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java @@ -130,18 +130,22 @@ public abstract class SegmentBase { */ public int getSegmentNum(long timeUs, long periodDurationUs) { final int firstSegmentNum = getFirstSegmentNum(); - int lowIndex = firstSegmentNum; - int highIndex = getLastSegmentNum(periodDurationUs); + final int segmentCount = getSegmentCount(periodDurationUs); + if (segmentCount == 0) { + return firstSegmentNum; + } if (segmentTimeline == null) { // All segments are of equal duration (with the possible exception of the last one). long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; int segmentNum = startNumber + (int) (timeUs / durationUs); // Ensure we stay within bounds. - return segmentNum < lowIndex ? lowIndex - : highIndex != DashSegmentIndex.INDEX_UNBOUNDED && segmentNum > highIndex ? highIndex - : segmentNum; + return segmentNum < firstSegmentNum ? firstSegmentNum + : segmentCount == DashSegmentIndex.INDEX_UNBOUNDED ? segmentNum + : Math.min(segmentNum, firstSegmentNum + segmentCount - 1); } else { - // The high index cannot be unbounded. Identify the segment using binary search. + // The index cannot be unbounded. Identify the segment using binary search. + int lowIndex = firstSegmentNum; + int highIndex = firstSegmentNum + segmentCount - 1; while (lowIndex <= highIndex) { int midIndex = lowIndex + (highIndex - lowIndex) / 2; long midTimeUs = getSegmentTimeUs(midIndex); @@ -165,7 +169,9 @@ public abstract class SegmentBase { long duration = segmentTimeline.get(sequenceNumber - startNumber).duration; return (duration * C.MICROS_PER_SECOND) / timescale; } else { - return sequenceNumber == getLastSegmentNum(periodDurationUs) + int segmentCount = getSegmentCount(periodDurationUs); + return segmentCount != DashSegmentIndex.INDEX_UNBOUNDED + && sequenceNumber == (getFirstSegmentNum() + segmentCount - 1) ? (periodDurationUs - getSegmentTimeUs(sequenceNumber)) : ((duration * C.MICROS_PER_SECOND) / timescale); } @@ -201,9 +207,9 @@ public abstract class SegmentBase { } /** - * @see DashSegmentIndex#getLastSegmentNum(long) + * @see DashSegmentIndex#getSegmentCount(long) */ - public abstract int getLastSegmentNum(long periodDurationUs); + public abstract int getSegmentCount(long periodDurationUs); /** * @see DashSegmentIndex#isExplicit() @@ -250,8 +256,8 @@ public abstract class SegmentBase { } @Override - public int getLastSegmentNum(long periodDurationUs) { - return startNumber + mediaSegments.size() - 1; + public int getSegmentCount(long periodDurationUs) { + return mediaSegments.size(); } @Override @@ -322,14 +328,14 @@ public abstract class SegmentBase { } @Override - public int getLastSegmentNum(long periodDurationUs) { + public int getSegmentCount(long periodDurationUs) { if (segmentTimeline != null) { - return segmentTimeline.size() + startNumber - 1; - } else if (periodDurationUs == C.TIME_UNSET) { - return DashSegmentIndex.INDEX_UNBOUNDED; - } else { + return segmentTimeline.size(); + } else if (periodDurationUs != C.TIME_UNSET) { long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; - return startNumber + (int) Util.ceilDivide(periodDurationUs, durationUs) - 1; + return (int) Util.ceilDivide(periodDurationUs, durationUs); + } else { + return DashSegmentIndex.INDEX_UNBOUNDED; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java index 083046d073..4ce49c5ffe 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java @@ -57,8 +57,8 @@ import com.google.android.exoplayer2.source.dash.DashSegmentIndex; } @Override - public int getLastSegmentNum(long periodDurationUs) { - return 0; + public int getSegmentCount(long periodDurationUs) { + return 1; } @Override From ec98bd9ea1d230c8b641c1d0dcff405b5677dfbd Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 15 Feb 2017 10:30:40 -0800 Subject: [PATCH 040/140] Work around broken AAC decoder EoS handling on L. SoftAAC2 would cause an exception to be thrown from dequeueOutputBuffer/releaseOutputBuffer after queueing an end-of-stream buffer for certain streams. The bug was introduced in L and fixed in L MR1, so the workaround is targeted to API 21. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147613659 --- .../mediacodec/MediaCodecRenderer.java | 63 +++++++++++++++++-- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 0330b13eb6..9baf974b37 100644 --- a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -183,6 +183,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private boolean codecNeedsAdaptationWorkaround; private boolean codecNeedsEosPropagationWorkaround; private boolean codecNeedsEosFlushWorkaround; + private boolean codecNeedsEosOutputExceptionWorkaround; private boolean codecNeedsMonoChannelCountWorkaround; private boolean codecNeedsAdaptationWorkaroundBuffer; private boolean shouldSkipAdaptationWorkaroundOutputBuffer; @@ -342,6 +343,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecNeedsAdaptationWorkaround = codecNeedsAdaptationWorkaround(codecName); codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName); codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName); + codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName); codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, format); try { long codecInitializingTimestamp = SystemClock.elapsedRealtime(); @@ -513,7 +515,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecNeedsAdaptationWorkaroundBuffer = false; shouldSkipAdaptationWorkaroundOutputBuffer = false; if (codecNeedsFlushWorkaround || (codecNeedsEosFlushWorkaround && codecReceivedEos)) { - // Workaround framework bugs. See [Internal: b/8347958, b/8578467, b/8543366, b/23361053]. releaseCodec(); maybeInitCodec(); } else if (codecReinitializationState != REINITIALIZATION_STATE_NONE) { @@ -867,7 +868,22 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { if (outputIndex < 0) { - outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs()); + if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) { + try { + outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, + getDequeueOutputBufferTimeoutUs()); + } catch (IllegalStateException e) { + processEndOfStream(); + if (outputStreamEnded) { + // Release the codec, as it's in an error state. + releaseCodec(); + } + return false; + } + } else { + outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, + getDequeueOutputBufferTimeoutUs()); + } if (outputIndex >= 0) { // We've dequeued a buffer. if (shouldSkipAdaptationWorkaroundOutputBuffer) { @@ -906,9 +922,27 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } - if (processOutputBuffer(positionUs, elapsedRealtimeUs, codec, outputBuffers[outputIndex], - outputIndex, outputBufferInfo.flags, outputBufferInfo.presentationTimeUs, - shouldSkipOutputBuffer)) { + boolean processedOutputBuffer; + if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) { + try { + processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs, codec, + outputBuffers[outputIndex], outputIndex, outputBufferInfo.flags, + outputBufferInfo.presentationTimeUs, shouldSkipOutputBuffer); + } catch (IllegalStateException e) { + processEndOfStream(); + if (outputStreamEnded) { + // Release the codec, as it's in an error state. + releaseCodec(); + } + return false; + } + } else { + processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs, codec, + outputBuffers[outputIndex], outputIndex, outputBufferInfo.flags, + outputBufferInfo.presentationTimeUs, shouldSkipOutputBuffer); + } + + if (processedOutputBuffer) { onProcessedOutputBuffer(outputBufferInfo.presentationTimeUs); outputIndex = C.INDEX_UNSET; return true; @@ -1010,6 +1044,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { *

* If true is returned, the renderer will work around the issue by releasing the decoder and * instantiating a new one rather than flushing the current instance. + *

+ * See [Internal: b/8347958, b/8543366]. * * @param name The name of the decoder. * @return True if the decoder is known to fail when flushed. @@ -1079,6 +1115,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { *

* If true is returned, the renderer will work around the issue by instantiating a new decoder * when this case occurs. + *

+ * See [Internal: b/8578467, b/23361053]. * * @param name The name of the decoder. * @return True if the decoder is known to behave incorrectly if flushed after receiving an input @@ -1091,6 +1129,21 @@ public abstract class MediaCodecRenderer extends BaseRenderer { || "OMX.amlogic.avc.decoder.awesome.secure".equals(name))); } + /** + * Returns whether the decoder may throw an {@link IllegalStateException} from + * {@link MediaCodec#dequeueOutputBuffer(MediaCodec.BufferInfo, long)} or + * {@link MediaCodec#releaseOutputBuffer(int, boolean)} after receiving an input + * buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set. + *

+ * See [Internal: b/17933838]. + * + * @param name The name of the decoder. + * @return True if the decoder may throw an exception after receiving an end-of-stream buffer. + */ + private static boolean codecNeedsEosOutputExceptionWorkaround(String name) { + return Util.SDK_INT == 21 && "OMX.google.aac.decoder".equals(name); + } + /** * Returns whether the decoder is known to set the number of audio channels in the output format * to 2 for the given input format, whilst only actually outputting a single channel. From 65d4b1cf5c75bf6416acb4836564745f5f57048e Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 Feb 2017 10:33:16 -0800 Subject: [PATCH 041/140] Make CeaUtil robust against malformed SEI data I've also added a TODO to not even bother trying to parse CEA from SEI NAL units if they're fully or partially encrypted, which it's possible to determine in the FMP4 extractor. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147613979 --- .../extractor/mp4/FragmentedMp4Extractor.java | 1 + .../android/exoplayer2/text/cea/CeaUtil.java | 45 +++++++++++++------ 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 8144880338..0beb644ff6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -1086,6 +1086,7 @@ public final class FragmentedMp4Extractor implements Extractor { output.sampleData(nalStartCode, 4); // Write the NAL unit type byte. output.sampleData(nalPrefix, 1); + // TODO: Don't try and process the SEI NAL unit if the payload is encrypted. processSeiNalUnitPayload = cea608TrackOutput != null && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]); sampleBytesWritten += 5; diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java index 3053debfcf..a39c8c8669 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text.cea; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -24,6 +25,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; */ public final class CeaUtil { + private static final String TAG = "CeaUtil"; + private static final int PAYLOAD_TYPE_CC = 4; private static final int COUNTRY_CODE = 0xB5; private static final int PROVIDER_CODE = 0x31; @@ -40,22 +43,15 @@ public final class CeaUtil { */ public static void consume(long presentationTimeUs, ParsableByteArray seiBuffer, TrackOutput output) { - int b; while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) { - // Parse payload type. - int payloadType = 0; - do { - b = seiBuffer.readUnsignedByte(); - payloadType += b; - } while (b == 0xFF); - // Parse payload size. - int payloadSize = 0; - do { - b = seiBuffer.readUnsignedByte(); - payloadSize += b; - } while (b == 0xFF); + int payloadType = readNon255TerminatedValue(seiBuffer); + int payloadSize = readNon255TerminatedValue(seiBuffer); // Process the payload. - if (isSeiMessageCea608(payloadType, payloadSize, seiBuffer)) { + if (payloadSize == -1 || payloadSize > seiBuffer.bytesLeft()) { + // This might occur if we're trying to read an encrypted SEI NAL unit. + Log.w(TAG, "Skipping remainder of malformed SEI NAL unit."); + seiBuffer.setPosition(seiBuffer.limit()); + } else if (isSeiMessageCea608(payloadType, payloadSize, seiBuffer)) { // Ignore country_code (1) + provider_code (2) + user_identifier (4) // + user_data_type_code (1). seiBuffer.skipBytes(8); @@ -76,6 +72,27 @@ public final class CeaUtil { } } + /** + * Reads a value from the provided buffer consisting of zero or more 0xFF bytes followed by a + * terminating byte not equal to 0xFF. The returned value is ((0xFF * N) + T), where N is the + * number of 0xFF bytes and T is the value of the terminating byte. + * + * @param buffer The buffer from which to read the value. + * @returns The read value, or -1 if the end of the buffer is reached before a value is read. + */ + private static int readNon255TerminatedValue(ParsableByteArray buffer) { + int b; + int value = 0; + do { + if (buffer.bytesLeft() == 0) { + return -1; + } + b = buffer.readUnsignedByte(); + value += b; + } while (b == 0xFF); + return value; + } + /** * Inspects an sei message to determine whether it contains CEA-608. *

From e74b729952a9b3018ee63f7dc4a8320ab8578de4 Mon Sep 17 00:00:00 2001 From: Sungmin Kim Date: Thu, 16 Feb 2017 21:23:28 +0900 Subject: [PATCH 042/140] fixed a typing error --- .../android/exoplayer2/trackselection/BaseTrackSelection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java index c81ffb441f..054ee7973f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java @@ -148,7 +148,7 @@ public abstract class BaseTrackSelection implements TrackSelection { } /** - * Returns whether the track at the specified index in the selection is blaclisted. + * Returns whether the track at the specified index in the selection is blacklisted. * * @param index The index of the track in the selection. * @param nowMs The current time in the timebase of {@link SystemClock#elapsedRealtime()}. From f8bb329ef2a668498262111157e90ebc91a82519 Mon Sep 17 00:00:00 2001 From: Johannes Schamburger Date: Thu, 16 Feb 2017 17:05:12 +0100 Subject: [PATCH 043/140] Make DrmSessionException constructor public to enable creating custom DrmSessionManager implementations. --- .../main/java/com/google/android/exoplayer2/drm/DrmSession.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java index 4d64187a8b..df9b1fffa0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java @@ -31,7 +31,7 @@ public interface DrmSession { /** Wraps the exception which is the cause of the error state. */ class DrmSessionException extends Exception { - DrmSessionException(Exception e) { + public DrmSessionException(Exception e) { super(e); } From 0d468ca7bf7382537f5540854dc268e8d1a4fa1b Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 16 Feb 2017 08:52:50 -0800 Subject: [PATCH 044/140] DASH: Support mspr:pro element Issue: #2386 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147725616 --- .../dash/manifest/DashManifestParser.java | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 1917399282..5cd0e593be 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -335,30 +335,35 @@ public class DashManifestParser extends DefaultHandler */ protected SchemeData parseContentProtection(XmlPullParser xpp) throws XmlPullParserException, IOException { + String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); + boolean isPlayReady = "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95".equals(schemeIdUri); byte[] data = null; UUID uuid = null; - boolean seenPsshElement = false; boolean requiresSecureDecoder = false; do { xpp.next(); - // The cenc:pssh element is defined in 23001-7:2015. - if (XmlPullParserUtil.isStartTag(xpp, "cenc:pssh") && xpp.next() == XmlPullParser.TEXT) { - seenPsshElement = true; - data = Base64.decode(xpp.getText(), Base64.DEFAULT); + if (data == null && XmlPullParserUtil.isStartTag(xpp, "cenc:pssh") + && xpp.next() == XmlPullParser.TEXT) { + // The cenc:pssh element is defined in 23001-7:2015. uuid = PsshAtomUtil.parseUuid(data); + if (uuid == null) { + Log.w(TAG, "Skipping malformed cenc:pssh data"); + } else { + data = Base64.decode(xpp.getText(), Base64.DEFAULT); + } + } else if (data == null && isPlayReady && XmlPullParserUtil.isStartTag(xpp, "mspr:pro") + && xpp.next() == XmlPullParser.TEXT) { + // The mspr:pro element is defined in DASH Content Protection using Microsoft PlayReady. + uuid = C.PLAYREADY_UUID; + data = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, + Base64.decode(xpp.getText(), Base64.DEFAULT)); } else if (XmlPullParserUtil.isStartTag(xpp, "widevine:license")) { String robustnessLevel = xpp.getAttributeValue(null, "robustness_level"); requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW"); } } while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection")); - if (!seenPsshElement) { - return null; - } else if (uuid != null) { - return new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder); - } else { - Log.w(TAG, "Skipped unsupported ContentProtection element"); - return null; - } + return data != null ? new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) + : null; } /** From 4cca2f2d0a7edc4b05d94fa6c71559bfa2a910e1 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 16 Feb 2017 09:49:27 -0800 Subject: [PATCH 045/140] Remove unnecessary null check. Issue #2462 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147731351 --- .../java/com/google/android/exoplayer2/demo/EventLogger.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index edc268ddb9..e39cd16743 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -101,9 +101,6 @@ import java.util.Locale; @Override public void onTimelineChanged(Timeline timeline, Object manifest) { - if (timeline == null) { - return; - } int periodCount = timeline.getPeriodCount(); int windowCount = timeline.getWindowCount(); Log.d(TAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount); From 636eecf4e78f78323206375443934db40966d7a1 Mon Sep 17 00:00:00 2001 From: twisstosin Date: Thu, 16 Feb 2017 21:30:37 +0100 Subject: [PATCH 046/140] Reverted Font Size Change --- library/src/main/res/layout/exo_playback_control_view.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src/main/res/layout/exo_playback_control_view.xml b/library/src/main/res/layout/exo_playback_control_view.xml index 531ee4c6fa..f8ef5a6fdd 100644 --- a/library/src/main/res/layout/exo_playback_control_view.xml +++ b/library/src/main/res/layout/exo_playback_control_view.xml @@ -58,7 +58,7 @@ Date: Thu, 16 Feb 2017 18:26:32 -0800 Subject: [PATCH 047/140] Use execute instead of submit ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147794314 --- .../google/android/exoplayer2/extractor/Extractor.java | 1 + .../com/google/android/exoplayer2/upstream/Loader.java | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java index 38b0325cba..de3dfd5266 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java @@ -102,4 +102,5 @@ public interface Extractor { * Releases all kept resources. */ void release(); + } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java index 64836dae4c..c9173d3756 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java @@ -199,7 +199,7 @@ public final class Loader implements LoaderErrorThrower { currentTask.cancel(true); } if (postLoadAction != null) { - downloadExecutorService.submit(postLoadAction); + downloadExecutorService.execute(postLoadAction); } downloadExecutorService.shutdown(); } @@ -260,7 +260,7 @@ public final class Loader implements LoaderErrorThrower { if (delayMillis > 0) { sendEmptyMessageDelayed(MSG_START, delayMillis); } else { - submitToExecutor(); + execute(); } } @@ -367,9 +367,9 @@ public final class Loader implements LoaderErrorThrower { } } - private void submitToExecutor() { + private void execute() { currentError = null; - downloadExecutorService.submit(currentTask); + downloadExecutorService.execute(currentTask); } private void finish() { From bc9dfa813911a5e9ffd4309c3cfb8bfdeac3a166 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 16 Feb 2017 19:00:00 -0800 Subject: [PATCH 048/140] Fix build ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147796398 --- .../java/com/google/android/exoplayer2/upstream/Loader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java index c9173d3756..bca90ddc5c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java @@ -334,7 +334,7 @@ public final class Loader implements LoaderErrorThrower { return; } if (msg.what == MSG_START) { - submitToExecutor(); + execute(); return; } if (msg.what == MSG_FATAL_ERROR) { From 14507c45036bb4057f4c3d35bb5beb790cdfee22 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 17 Feb 2017 06:51:29 -0800 Subject: [PATCH 049/140] Fix NPE parsing ContentProtection element ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147833328 --- .../exoplayer2/source/dash/manifest/DashManifestParser.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 5cd0e593be..d4338fd812 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -345,18 +345,18 @@ public class DashManifestParser extends DefaultHandler if (data == null && XmlPullParserUtil.isStartTag(xpp, "cenc:pssh") && xpp.next() == XmlPullParser.TEXT) { // The cenc:pssh element is defined in 23001-7:2015. + data = Base64.decode(xpp.getText(), Base64.DEFAULT); uuid = PsshAtomUtil.parseUuid(data); if (uuid == null) { Log.w(TAG, "Skipping malformed cenc:pssh data"); - } else { - data = Base64.decode(xpp.getText(), Base64.DEFAULT); + data = null; } } else if (data == null && isPlayReady && XmlPullParserUtil.isStartTag(xpp, "mspr:pro") && xpp.next() == XmlPullParser.TEXT) { // The mspr:pro element is defined in DASH Content Protection using Microsoft PlayReady. - uuid = C.PLAYREADY_UUID; data = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, Base64.decode(xpp.getText(), Base64.DEFAULT)); + uuid = C.PLAYREADY_UUID; } else if (XmlPullParserUtil.isStartTag(xpp, "widevine:license")) { String robustnessLevel = xpp.getAttributeValue(null, "robustness_level"); requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW"); From c3d7eecd1f64ae6cdb120cb543d4994a10c2d02c Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 17 Feb 2017 07:57:49 -0800 Subject: [PATCH 050/140] Fix ChunkExtractorWrapper.init(TrackOutput) calls with null TrackOutput after extractor initialized InitializationChunk calls init(null). When the initialization and index data is separate they need to be loaded separately which results to two init(null) calls. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147837985 --- .../android/exoplayer2/source/chunk/ChunkExtractorWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java index 489f63be2b..2a641b80a6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java @@ -88,7 +88,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput extractorInitialized = true; } else { extractor.seek(0, 0); - if (sampleFormat != null) { + if (sampleFormat != null && trackOutput != null) { trackOutput.format(sampleFormat); } } From e9399f8684363cf0fc7e2a425513341b9d0b77f0 Mon Sep 17 00:00:00 2001 From: Daniel Santiago Date: Fri, 17 Feb 2017 12:44:13 -0800 Subject: [PATCH 051/140] Added flags to the mp3 extractor to control behavior of the extractor. Added FLAG_ENABLE_CONSTANT_BITRATE_SEEKING to let the extractor know that the CBR seeker is desired in the case where the seeker has determine the track is not seekable For example, in the case where the track has a Xing Header but no content table. --- .../extractor/mp3/Mp3Extractor.java | 39 +++++++++++++++++-- .../exoplayer2/source/hls/HlsMediaChunk.java | 2 +- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index ff84c7da25..e38ae148bd 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -31,8 +31,13 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; + +import android.support.annotation.IntDef; + import java.io.EOFException; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Extracts data from an MP3 file. @@ -51,6 +56,19 @@ public final class Mp3Extractor implements Extractor { }; + /** + * Flags controlling the behavior of the extractor. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}) + public @interface Flags {} + + /** + * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would + * otherwise not be possible. + */ + public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; + /** * The maximum number of bytes to search when synchronizing, before giving up. */ @@ -72,6 +90,9 @@ public final class Mp3Extractor implements Extractor { private static final int INFO_HEADER = Util.getIntegerCodeForString("Info"); private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI"); + @Flags + private final int flags; + private final long forcedFirstSampleTimestampUs; private final ParsableByteArray scratch; private final MpegAudioHeader synchronizedHeader; @@ -93,16 +114,27 @@ public final class Mp3Extractor implements Extractor { * Constructs a new {@link Mp3Extractor}. */ public Mp3Extractor() { - this(C.TIME_UNSET); + this(0); } /** * Constructs a new {@link Mp3Extractor}. * + * @param flags Flags that control the extractor's behavior. + */ + public Mp3Extractor(@Flags int flags) { + this(flags, C.TIME_UNSET); + } + + /** + * Constructs a new {@link Mp3Extractor}. + * + * @param flags Flags that control the extractor's behavior. * @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or * {@link C#TIME_UNSET} if forcing is not required. */ - public Mp3Extractor(long forcedFirstSampleTimestampUs) { + public Mp3Extractor(@Flags int flags, long forcedFirstSampleTimestampUs) { + this.flags = flags; this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; scratch = new ParsableByteArray(SCRATCH_LENGTH); synchronizedHeader = new MpegAudioHeader(); @@ -350,7 +382,8 @@ public final class Mp3Extractor implements Extractor { } } - if (seeker == null) { + if (seeker == null || (!seeker.isSeekable() + && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { // Repopulate the synchronized header in case we had to skip an invalid seeking header, which // would give an invalid CBR bitrate. input.resetPeekPosition(); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index a3e3559724..5885797896 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -379,7 +379,7 @@ import java.util.concurrent.atomic.AtomicInteger; || lastPathSegment.endsWith(EC3_FILE_EXTENSION)) { extractor = new Ac3Extractor(startTimeUs); } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { - extractor = new Mp3Extractor(startTimeUs); + extractor = new Mp3Extractor(0, startTimeUs); } else { throw new IllegalArgumentException("Unkown extension for audio file: " + lastPathSegment); } From 37c15e7ceeeddeadf9d8b7afba991fa074f9f94c Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 20 Feb 2017 11:33:43 +0000 Subject: [PATCH 052/140] Minor stylistic tweaks --- .../android/exoplayer2/extractor/mp3/Mp3Extractor.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index e38ae148bd..ef0fdd9393 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -62,7 +62,6 @@ public final class Mp3Extractor implements Extractor { @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}) public @interface Flags {} - /** * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would * otherwise not be possible. @@ -90,9 +89,7 @@ public final class Mp3Extractor implements Extractor { private static final int INFO_HEADER = Util.getIntegerCodeForString("Info"); private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI"); - @Flags - private final int flags; - + @Flags private final int flags; private final long forcedFirstSampleTimestampUs; private final ParsableByteArray scratch; private final MpegAudioHeader synchronizedHeader; From edae29dff754fe4bd410fdb6fcedd60ed2266b9c Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 20 Feb 2017 11:35:06 +0000 Subject: [PATCH 053/140] Fix import order --- .../google/android/exoplayer2/extractor/mp3/Mp3Extractor.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index ef0fdd9393..00394f7912 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.mp3; +import android.support.annotation.IntDef; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -31,9 +32,6 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; - -import android.support.annotation.IntDef; - import java.io.EOFException; import java.io.IOException; import java.lang.annotation.Retention; From f16058422df7f42f7a4a20ec9fa4a4128f55fd88 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 20 Feb 2017 12:51:56 +0000 Subject: [PATCH 054/140] Fix import order --- .../src/main/java/com/google/android/exoplayer2/text/Cue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/src/main/java/com/google/android/exoplayer2/text/Cue.java index 84b67928bb..3210ffd9a4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.text; -import android.graphics.Color; import android.graphics.Bitmap; +import android.graphics.Color; import android.support.annotation.IntDef; import android.text.Layout.Alignment; import java.lang.annotation.Retention; From 21923ae1faa1db85256a016721bc238a20d2d2b8 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 20 Feb 2017 12:55:07 +0000 Subject: [PATCH 055/140] m --- .../com/google/android/exoplayer2/ui/SubtitlePainter.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index 3e9fb3e68b..555aa84531 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -157,6 +157,7 @@ import com.google.android.exoplayer2.util.Util; if (!applyEmbeddedStyles) { // Strip out any embedded styling. cueText = cueText.toString(); + windowColor = style.windowColor; } } else { cueBitmap = cue.bitmap; @@ -190,7 +191,7 @@ import com.google.android.exoplayer2.util.Util; this.cueText = cueText; this.cueTextAlignment = cue.textAlignment; - this.cueBitmap = cue.bitmap; + this.cueBitmap = cueBitmap; this.cueLine = cue.line; this.cueLineType = cue.lineType; this.cueLineAnchor = cue.lineAnchor; @@ -200,7 +201,7 @@ import com.google.android.exoplayer2.util.Util; this.applyEmbeddedStyles = applyEmbeddedStyles; this.foregroundColor = style.foregroundColor; this.backgroundColor = style.backgroundColor; - this.windowColor = style.windowColor; + this.windowColor = windowColor; this.edgeType = style.edgeType; this.edgeColor = style.edgeColor; this.textPaint.setTypeface(style.typeface); From 31513202df8c186764ad5d2d1d14a8a7fa49a7f6 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 20 Feb 2017 13:01:00 +0000 Subject: [PATCH 056/140] Fix subtitle painter issues --- .../android/exoplayer2/ui/SubtitlePainter.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index 555aa84531..04a6bafd3d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -65,9 +65,9 @@ import com.google.android.exoplayer2.util.Util; private final Paint paint; // Previous input variables. - private Bitmap cueBitmap; private CharSequence cueText; private Alignment cueTextAlignment; + private Bitmap cueBitmap; private float cueLine; @Cue.LineType private int cueLineType; @@ -148,12 +148,14 @@ import com.google.android.exoplayer2.util.Util; boolean isTextCue = cue.bitmap == null; CharSequence cueText = null; Bitmap cueBitmap = null; + int windowColor = Color.BLACK; if (isTextCue) { cueText = cue.text; if (TextUtils.isEmpty(cueText)) { // Nothing to draw. return; } + windowColor = cue.windowColorSet ? cue.windowColor : style.windowColor; if (!applyEmbeddedStyles) { // Strip out any embedded styling. cueText = cueText.toString(); @@ -174,7 +176,7 @@ import com.google.android.exoplayer2.util.Util; && this.applyEmbeddedStyles == applyEmbeddedStyles && this.foregroundColor == style.foregroundColor && this.backgroundColor == style.backgroundColor - && this.windowColor == style.windowColor + && this.windowColor == windowColor && this.edgeType == style.edgeType && this.edgeColor == style.edgeColor && Util.areEqual(this.textPaint.getTypeface(), style.typeface) @@ -275,7 +277,7 @@ import com.google.android.exoplayer2.util.Util; if (cueLine >= 0) { anchorPosition = Math.round(cueLine * firstLineHeight) + parentTop; } else { - anchorPosition = Math.round(cueLine * firstLineHeight) + parentBottom; + anchorPosition = Math.round((cueLine + 1) * firstLineHeight) + parentBottom; } } textTop = cueLineAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textHeight @@ -309,8 +311,8 @@ import com.google.android.exoplayer2.util.Util; int height = (int) (width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth())); int x = (int) (cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width) : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX); - int y = (int) (cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - width) - : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (width / 2)) : anchorY); + int y = (int) (cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - height) + : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2)) : anchorY); bitmapRect = new Rect(x, y, x + width, y + height); } From 5fbf10969453d823ebe85e39573712669695ef41 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 20 Feb 2017 13:03:03 +0000 Subject: [PATCH 057/140] Use Math.round instead of floor. --- .../com/google/android/exoplayer2/ui/SubtitlePainter.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index 04a6bafd3d..6a1f31d270 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -307,11 +307,11 @@ import com.google.android.exoplayer2.util.Util; int parentHeight = parentBottom - parentTop; float anchorX = parentLeft + (parentWidth * cuePosition); float anchorY = parentTop + (parentHeight * cueLine); - int width = (int) (parentWidth * cueSize); - int height = (int) (width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth())); - int x = (int) (cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width) + int width = Math.round(parentWidth * cueSize); + int height = Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth())); + int x = Math.round(cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width) : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX); - int y = (int) (cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - height) + int y = Math.round(cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - height) : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2)) : anchorY); bitmapRect = new Rect(x, y, x + width, y + height); } From 539072dbf4236610be56224c836b6b0d1677fdb3 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 20 Feb 2017 13:09:40 +0000 Subject: [PATCH 058/140] Remove useless Cue constructor --- .../java/com/google/android/exoplayer2/text/Cue.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/src/main/java/com/google/android/exoplayer2/text/Cue.java index 3210ffd9a4..96cd76a957 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -177,16 +177,6 @@ public class Cue { */ public final int windowColor; - /** - * Constructs an image cue whose type parameters are set to {@link #TYPE_UNSET} and whose - * dimension parameters are set to {@link #DIMEN_UNSET}. - * - * @param bitmap See {@link #bitmap}. - */ - public Cue(Bitmap bitmap) { - this(bitmap, DIMEN_UNSET, TYPE_UNSET, DIMEN_UNSET, TYPE_UNSET, DIMEN_UNSET); - } - /** * Creates an image cue. * From 11c16d83fdf65c6f35d8852e08baa7b75ffe1a47 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 20 Feb 2017 13:13:13 +0000 Subject: [PATCH 059/140] Final nit fixes for Cue/SubtitlePainter --- .../src/main/java/com/google/android/exoplayer2/text/Cue.java | 3 ++- .../java/com/google/android/exoplayer2/ui/SubtitlePainter.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/src/main/java/com/google/android/exoplayer2/text/Cue.java index 96cd76a957..176b8ea815 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -180,6 +180,7 @@ public class Cue { /** * Creates an image cue. * + * @param bitmap See {@link #bitmap}. * @param horizontalPosition The position of the horizontal anchor within the viewport, expressed * as a fraction of the viewport width. * @param horizontalPositionAnchor The horizontal anchor. One of {@link #ANCHOR_TYPE_START}, @@ -197,7 +198,7 @@ public class Cue { } /** - * Constructs a text cue whose {@link #textAlignment} is null, whose type parameters are set to + * Creates a text cue whose {@link #textAlignment} is null, whose type parameters are set to * {@link #TYPE_UNSET} and whose dimension parameters are set to {@link #DIMEN_UNSET}. * * @param text See {@link #text}. diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index 6a1f31d270..5ca97403f1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -325,7 +325,7 @@ import com.google.android.exoplayer2.util.Util; } private void drawTextLayout(Canvas canvas) { - final StaticLayout layout = textLayout; + StaticLayout layout = textLayout; if (layout == null) { // Nothing to draw. return; From e0f7a124889f340f6b9db58efd3d4f577cb12126 Mon Sep 17 00:00:00 2001 From: wasabeef Date: Wed, 22 Feb 2017 16:01:43 +0900 Subject: [PATCH 060/140] Added support for CENC ClearKey --- demo/src/main/assets/media.exolist.json | 12 +++++++++++ .../demo/SampleChooserActivity.java | 2 ++ .../java/com/google/android/exoplayer2/C.java | 6 ++++++ .../drm/DefaultDrmSessionManager.java | 21 +++++++++++++++++++ 4 files changed, 41 insertions(+) diff --git a/demo/src/main/assets/media.exolist.json b/demo/src/main/assets/media.exolist.json index 6fba5bd65b..dd88f206c1 100644 --- a/demo/src/main/assets/media.exolist.json +++ b/demo/src/main/assets/media.exolist.json @@ -277,6 +277,18 @@ } ] }, + { + "name": "ClearKey DASH", + "samples": [ + { + "name": "Big Buck Bunny (CENC ClearKey)", + "uri": "http://html5.cablelabs.com:8100/cenc/ck/dash.mpd", + "extension": "mpd", + "drm_scheme": "cenc", + "drm_license_url": "https://wasabeef.jp/demos/cenc-ck-dash.json" + } + ] + }, { "name": "SmoothStreaming", "samples": [ diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 946181284f..7377aa416d 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -267,6 +267,8 @@ public class SampleChooserActivity extends Activity { return C.WIDEVINE_UUID; case "playready": return C.PLAYREADY_UUID; + case "cenc": + return C.CENC_UUID; default: try { return UUID.fromString(typeString); diff --git a/library/src/main/java/com/google/android/exoplayer2/C.java b/library/src/main/java/com/google/android/exoplayer2/C.java index 0b1c33bfc9..ab2840d9e4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/src/main/java/com/google/android/exoplayer2/C.java @@ -443,6 +443,12 @@ public final class C { */ public static final UUID UUID_NIL = new UUID(0L, 0L); + /** + * UUID for the PSSH box and MPEG-DASH Content Protection. + * W3C. + */ + public static final UUID CENC_UUID = new UUID(0x1077EFECC0B24D02L, 0xACE33C1E52E2FB4BL); + /** * UUID for the Widevine DRM scheme. *

diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 1cd8d8464d..c6480a0d09 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -102,6 +103,11 @@ public class DefaultDrmSessionManager implements DrmSe /** Releases an existing offline license. */ public static final int MODE_RELEASE = 3; + /** + * The format to use when ClearKey encryption. + */ + private static final String CENC_INIT_DATA_FORMAT = "cenc"; + private static final String TAG = "OfflineDrmSessionMngr"; private static final int MSG_PROVISION = 0; @@ -337,6 +343,21 @@ public class DefaultDrmSessionManager implements DrmSe schemeInitData = psshData; } } + if (C.CENC_UUID.equals(uuid)) { + // If "video/mp4" and "audio/mp4" are not supported as CENC schema, change it to "cenc". + // Before 7.1.x in API 25, "video/mp4" and "audio/mp4" are not supported. + if (MimeTypes.VIDEO_MP4.equals(schemeMimeType) || MimeTypes.AUDIO_MP4.equals( + schemeMimeType)) { + if (Util.SDK_INT >= 26) { + // Nothing to do. + } else if (Util.SDK_INT == 25 && !MediaDrm.isCryptoSchemeSupported(uuid, + schemeMimeType)) { + schemeMimeType = CENC_INIT_DATA_FORMAT; + } else if (Util.SDK_INT <= 24) { + schemeMimeType = CENC_INIT_DATA_FORMAT; + } + } + } } state = STATE_OPENING; openInternal(true); From 17762ebaa2f96ddb7709fb04aa740d538657583d Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 20 Feb 2017 06:33:23 -0800 Subject: [PATCH 061/140] Reformat @IntDef field/return type annotations. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148018580 --- .../exoplayer2/ExoPlaybackException.java | 3 +-- .../audio/SimpleDecoderAudioRenderer.java | 3 +-- .../decoder/DecoderInputBuffer.java | 3 +-- .../android/exoplayer2/drm/DrmSession.java | 3 +-- .../drm/UnsupportedDrmException.java | 3 +-- .../extractor/mp4/FragmentedMp4Extractor.java | 3 +-- .../extractor/mp4/Mp4Extractor.java | 3 +-- .../exoplayer2/extractor/mp4/Track.java | 3 +-- .../ts/DefaultTsPayloadReaderFactory.java | 3 +-- .../exoplayer2/source/MergingMediaSource.java | 3 +-- .../source/hls/playlist/HlsMediaPlaylist.java | 3 +-- .../source/hls/playlist/HlsPlaylist.java | 3 +-- .../exoplayer2/text/CaptionStyleCompat.java | 3 +-- .../exoplayer2/text/ttml/TtmlStyle.java | 21 +++++++------------ .../text/webvtt/WebvttCssStyle.java | 21 +++++++------------ .../android/exoplayer2/upstream/DataSpec.java | 3 +-- .../exoplayer2/upstream/HttpDataSource.java | 3 +-- 17 files changed, 29 insertions(+), 58 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java index 72ac72e981..ca7367f1b0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java @@ -56,8 +56,7 @@ public final class ExoPlaybackException extends Exception { * The type of the playback failure. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER} and * {@link #TYPE_UNEXPECTED}. */ - @Type - public final int type; + @Type public final int type; /** * If {@link #type} is {@link #TYPE_RENDERER}, this is the index of the renderer. diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 9e75145626..3ca8c37e21 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -83,8 +83,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements private DrmSession drmSession; private DrmSession pendingDrmSession; - @ReinitializationState - private int decoderReinitializationState; + @ReinitializationState private int decoderReinitializationState; private boolean decoderReceivedBuffers; private boolean audioTrackNeedsConfigure; diff --git a/library/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java b/library/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java index b76f3e8d0c..84c89de427 100644 --- a/library/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java +++ b/library/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java @@ -61,8 +61,7 @@ public class DecoderInputBuffer extends Buffer { */ public long timeUs; - @BufferReplacementMode - private final int bufferReplacementMode; + @BufferReplacementMode private final int bufferReplacementMode; /** * @param bufferReplacementMode Determines the behavior of {@link #ensureSpaceForWrite(int)}. One diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java index df9b1fffa0..538db9e1d9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java @@ -70,8 +70,7 @@ public interface DrmSession { * @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING}, * {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}. */ - @State - int getState(); + @State int getState(); /** * Returns a {@link ExoMediaCrypto} for the open session. diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/UnsupportedDrmException.java b/library/src/main/java/com/google/android/exoplayer2/drm/UnsupportedDrmException.java index 505750efaa..f0e748d722 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/UnsupportedDrmException.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/UnsupportedDrmException.java @@ -43,8 +43,7 @@ public final class UnsupportedDrmException extends Exception { /** * Either {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}. */ - @Reason - public final int reason; + @Reason public final int reason; /** * @param reason {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}. diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 0beb644ff6..4069b9a25a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -117,8 +117,7 @@ public final class FragmentedMp4Extractor implements Extractor { private static final int STATE_READING_SAMPLE_CONTINUE = 4; // Workarounds. - @Flags - private final int flags; + @Flags private final int flags; private final Track sideloadedTrack; // Track-linked data bundle, accessible as a whole through trackID. diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 0c990f5747..d0e770abdc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -83,8 +83,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { private final ParsableByteArray atomHeader; private final Stack containerAtoms; - @State - private int parserState; + @State private int parserState; private int atomType; private long atomSize; private int atomHeaderBytesRead; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java index c723704d37..d673564dc4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java @@ -75,8 +75,7 @@ public final class Track { * One of {@code TRANSFORMATION_*}. Defines the transformation to apply before outputting each * sample. */ - @Transformation - public final int sampleTransformation; + @Transformation public final int sampleTransformation; /** * Track encryption boxes for the different track sample descriptions. Entries may be null. diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index c798494e42..4e94891889 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -40,8 +40,7 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact public static final int FLAG_DETECT_ACCESS_UNITS = 8; public static final int FLAG_IGNORE_SPLICE_INFO_STREAM = 16; - @Flags - private final int flags; + @Flags private final int flags; public DefaultTsPayloadReaderFactory() { this(0); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index 417483cebc..6f37165916 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -57,8 +57,7 @@ public final class MergingMediaSource implements MediaSource { * The reason the merge failed. One of {@link #REASON_WINDOWS_ARE_DYNAMIC} and * {@link #REASON_PERIOD_COUNT_MISMATCH}. */ - @Reason - public final int reason; + @Reason public final int reason; /** * @param reason The reason the merge failed. One of {@link #REASON_WINDOWS_ARE_DYNAMIC} and diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index b8d8d69af4..9ef28bdb8d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -78,8 +78,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public static final int PLAYLIST_TYPE_VOD = 1; public static final int PLAYLIST_TYPE_EVENT = 2; - @PlaylistType - public final int playlistType; + @PlaylistType public final int playlistType; public final long startOffsetUs; public final long startTimeUs; public final boolean hasDiscontinuitySequence; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java index fb62d9978e..aecd2fb324 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java @@ -34,8 +34,7 @@ public abstract class HlsPlaylist { public static final int TYPE_MEDIA = 1; public final String baseUri; - @Type - public final int type; + @Type public final int type; protected HlsPlaylist(String baseUri, @Type int type) { this.baseUri = baseUri; diff --git a/library/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java b/library/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java index b7a75ed679..51f5ad0a64 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java @@ -94,8 +94,7 @@ public final class CaptionStyleCompat { *
  • {@link #EDGE_TYPE_DEPRESSED} * */ - @EdgeType - public final int edgeType; + @EdgeType public final int edgeType; /** * The preferred edge color, if using an edge type other than {@link #EDGE_TYPE_NONE}. diff --git a/library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java b/library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java index e4c36be03a..90f93d5b21 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java @@ -56,16 +56,11 @@ import java.lang.annotation.RetentionPolicy; private boolean hasFontColor; private int backgroundColor; private boolean hasBackgroundColor; - @OptionalBoolean - private int linethrough; - @OptionalBoolean - private int underline; - @OptionalBoolean - private int bold; - @OptionalBoolean - private int italic; - @FontSizeUnit - private int fontSizeUnit; + @OptionalBoolean private int linethrough; + @OptionalBoolean private int underline; + @OptionalBoolean private int bold; + @OptionalBoolean private int italic; + @FontSizeUnit private int fontSizeUnit; private float fontSize; private String id; private TtmlStyle inheritableStyle; @@ -85,8 +80,7 @@ import java.lang.annotation.RetentionPolicy; * @return {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link #STYLE_BOLD}, {@link #STYLE_BOLD} * or {@link #STYLE_BOLD_ITALIC}. */ - @StyleFlags - public int getStyle() { + @StyleFlags public int getStyle() { if (bold == UNSPECIFIED && italic == UNSPECIFIED) { return UNSPECIFIED; } @@ -255,8 +249,7 @@ import java.lang.annotation.RetentionPolicy; return this; } - @FontSizeUnit - public int getFontSizeUnit() { + @FontSizeUnit public int getFontSizeUnit() { return fontSizeUnit; } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java b/library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java index 157174a8f0..10c17e2888 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java @@ -69,16 +69,11 @@ import java.util.List; private boolean hasFontColor; private int backgroundColor; private boolean hasBackgroundColor; - @OptionalBoolean - private int linethrough; - @OptionalBoolean - private int underline; - @OptionalBoolean - private int bold; - @OptionalBoolean - private int italic; - @FontSizeUnit - private int fontSizeUnit; + @OptionalBoolean private int linethrough; + @OptionalBoolean private int underline; + @OptionalBoolean private int bold; + @OptionalBoolean private int italic; + @FontSizeUnit private int fontSizeUnit; private float fontSize; private Layout.Alignment textAlign; @@ -162,8 +157,7 @@ import java.util.List; * @return {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link #STYLE_BOLD}, {@link #STYLE_BOLD} * or {@link #STYLE_BOLD_ITALIC}. */ - @StyleFlags - public int getStyle() { + @StyleFlags public int getStyle() { if (bold == UNSPECIFIED && italic == UNSPECIFIED) { return UNSPECIFIED; } @@ -260,8 +254,7 @@ import java.util.List; return this; } - @FontSizeUnit - public int getFontSizeUnit() { + @FontSizeUnit public int getFontSizeUnit() { return fontSizeUnit; } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index 133e71f6e2..c9ff19aee6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -82,8 +82,7 @@ public final class DataSpec { * Request flags. Currently {@link #FLAG_ALLOW_GZIP} and * {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} are the only supported flags. */ - @Flags - public final int flags; + @Flags public final int flags; /** * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null. diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java index a988cf1a33..1f88828f28 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java @@ -149,8 +149,7 @@ public interface HttpDataSource extends DataSource { public static final int TYPE_READ = 2; public static final int TYPE_CLOSE = 3; - @Type - public final int type; + @Type public final int type; /** * The {@link DataSpec} associated with the current connection. From 55a3fca6f990d3b6d0097d018daac4ec4cd7bcab Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Feb 2017 08:15:52 -0800 Subject: [PATCH 062/140] Clean up position restoration logic in demo app ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148024082 --- .../android/exoplayer2/demo/PlayerActivity.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 6c7b72522a..ba9c9352d8 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -110,7 +110,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay private DefaultTrackSelector trackSelector; private TrackSelectionHelper trackSelectionHelper; private DebugTextViewHelper debugViewHelper; - private boolean playerNeedsSource; + private boolean needRetrySource; private boolean shouldAutoPlay; private int resumeWindow; @@ -229,7 +229,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay private void initializePlayer() { Intent intent = getIntent(); - if (player == null) { + boolean needNewPlayer = player == null; + if (needNewPlayer) { boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false); UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA) ? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null; @@ -272,9 +273,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay player.setPlayWhenReady(shouldAutoPlay); debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper.start(); - playerNeedsSource = true; } - if (playerNeedsSource) { + if (needNewPlayer || needRetrySource) { String action = intent.getAction(); Uri[] uris; String[] extensions; @@ -310,7 +310,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay player.seekTo(resumeWindow, resumePosition); } player.prepare(mediaSource, !haveResumePosition, false); - playerNeedsSource = false; + needRetrySource = false; updateButtonVisibilities(); } } @@ -419,7 +419,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay @Override public void onPositionDiscontinuity() { - if (playerNeedsSource) { + if (needRetrySource) { // This will only occur if the user has performed a seek whilst in the error state. Update the // resume position so that if the user then retries, playback will resume from the position to // which they seeked. @@ -460,7 +460,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay if (errorString != null) { showToast(errorString); } - playerNeedsSource = true; + needRetrySource = true; if (isBehindLiveWindow(e)) { clearResumePosition(); initializePlayer(); @@ -492,7 +492,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay private void updateButtonVisibilities() { debugRootView.removeAllViews(); - retryButton.setVisibility(playerNeedsSource ? View.VISIBLE : View.GONE); + retryButton.setVisibility(needRetrySource ? View.VISIBLE : View.GONE); debugRootView.addView(retryButton); if (player == null) { From 72e1eae6f55580ecae06f649078d051e7f6f1f0f Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 20 Feb 2017 08:45:13 -0800 Subject: [PATCH 063/140] Discard preparation chunk if track selection does not include it This avoids breaking the player if the first variant is not supported by the device. Issue:#2353 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148025791 --- .../exoplayer2/source/hls/HlsSampleStreamWrapper.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 538acbeabf..6980fdd7a4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -182,6 +182,7 @@ import java.util.LinkedList; } } // Enable new tracks. + TrackSelection primaryTrackSelection = null; boolean selectedNewTracks = false; for (int i = 0; i < selections.length; i++) { if (streams[i] == null && selections[i] != null) { @@ -189,6 +190,7 @@ import java.util.LinkedList; int group = trackGroups.indexOf(selection.getTrackGroup()); setTrackGroupEnabledState(group, true); if (group == primaryTrackGroupIndex) { + primaryTrackSelection = selection; chunkSource.selectTracks(selection); } streams[i] = new HlsSampleStream(this, group); @@ -205,6 +207,14 @@ import java.util.LinkedList; sampleQueues.valueAt(i).disable(); } } + if (primaryTrackSelection != null && !mediaChunks.isEmpty()) { + primaryTrackSelection.updateSelectedTrack(0); + int chunkIndex = chunkSource.getTrackGroup().indexOf(mediaChunks.getLast().trackFormat); + if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) { + // The loaded preparation chunk does match the selection. We discard it. + seekTo(lastSeekPositionUs); + } + } } // Cancel requests if necessary. if (enabledTrackCount == 0) { From a84216c3a975d9abadfd4868186d23bad131d068 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Feb 2017 09:19:34 -0800 Subject: [PATCH 064/140] Allow enabling of EMSG/608 outputs on DefaultDashChunkSource Issue #2176 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148027655 --- .../extractor/mp4/FragmentedMp4Extractor.java | 1 - .../source/dash/DashChunkSource.java | 3 +- .../source/dash/DashMediaPeriod.java | 2 +- .../source/dash/DefaultDashChunkSource.java | 28 +++++++++++++------ 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 4069b9a25a..6a5662ce10 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -1085,7 +1085,6 @@ public final class FragmentedMp4Extractor implements Extractor { output.sampleData(nalStartCode, 4); // Write the NAL unit type byte. output.sampleData(nalPrefix, 1); - // TODO: Don't try and process the SEI NAL unit if the payload is encrypted. processSeiNalUnitPayload = cea608TrackOutput != null && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]); sampleBytesWritten += 5; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java index 4c943abb48..72f728092c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java @@ -29,7 +29,8 @@ public interface DashChunkSource extends ChunkSource { DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, DashManifest manifest, int periodIndex, int adaptationSetIndex, - TrackSelection trackSelection, long elapsedRealtimeOffsetMs); + TrackSelection trackSelection, long elapsedRealtimeOffsetMs, + boolean enableEventMessageTrack, boolean enableCea608Track); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index 4a24c7c176..c1d52c7a77 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -203,7 +203,7 @@ import java.util.List; AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex); DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource( manifestLoaderErrorThrower, manifest, index, adaptationSetIndex, selection, - elapsedRealtimeOffset); + elapsedRealtimeOffset, false, false); return new ChunkSampleStream<>(adaptationSet.type, chunkSource, this, allocator, positionUs, minLoadableRetryCount, eventDispatcher); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 4548bc75f8..7dd1294c22 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -70,11 +70,12 @@ public class DefaultDashChunkSource implements DashChunkSource { @Override public DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, DashManifest manifest, int periodIndex, int adaptationSetIndex, - TrackSelection trackSelection, long elapsedRealtimeOffsetMs) { + TrackSelection trackSelection, long elapsedRealtimeOffsetMs, + boolean enableEventMessageTrack, boolean enableCea608Track) { DataSource dataSource = dataSourceFactory.createDataSource(); return new DefaultDashChunkSource(manifestLoaderErrorThrower, manifest, periodIndex, adaptationSetIndex, trackSelection, dataSource, elapsedRealtimeOffsetMs, - maxSegmentsPerLoad); + maxSegmentsPerLoad, enableEventMessageTrack, enableCea608Track); } } @@ -106,10 +107,15 @@ public class DefaultDashChunkSource implements DashChunkSource { * @param maxSegmentsPerLoad The maximum number of segments to combine into a single request. * Note that segments will only be combined if their {@link Uri}s are the same and if their * data ranges are adjacent. + * @param enableEventMessageTrack Whether the chunks generated by the source may output an event + * message track. + * @param enableEventMessageTrack Whether the chunks generated by the source may output a CEA-608 + * track. */ public DefaultDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, DashManifest manifest, int periodIndex, int adaptationSetIndex, TrackSelection trackSelection, - DataSource dataSource, long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad) { + DataSource dataSource, long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad, + boolean enableEventMessageTrack, boolean enableCea608Track) { this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.manifest = manifest; this.adaptationSetIndex = adaptationSetIndex; @@ -126,7 +132,7 @@ public class DefaultDashChunkSource implements DashChunkSource { for (int i = 0; i < representationHolders.length; i++) { Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); representationHolders[i] = new RepresentationHolder(periodDurationUs, representation, - adaptationSet.type); + enableEventMessageTrack, enableCea608Track, adaptationSet.type); } } @@ -364,7 +370,6 @@ public class DefaultDashChunkSource implements DashChunkSource { protected static final class RepresentationHolder { - public final int trackType; public final ChunkExtractorWrapper extractorWrapper; public Representation representation; @@ -374,10 +379,9 @@ public class DefaultDashChunkSource implements DashChunkSource { private int segmentNumShift; public RepresentationHolder(long periodDurationUs, Representation representation, - int trackType) { + boolean enableEventMessageTrack, boolean enableCea608Track, int trackType) { this.periodDurationUs = periodDurationUs; this.representation = representation; - this.trackType = trackType; String containerMimeType = representation.format.containerMimeType; if (mimeTypeIsRawText(containerMimeType)) { extractorWrapper = null; @@ -388,8 +392,14 @@ public class DefaultDashChunkSource implements DashChunkSource { } else if (mimeTypeIsWebm(containerMimeType)) { extractor = new MatroskaExtractor(); } else { - extractor = new FragmentedMp4Extractor(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK - | FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK); + int flags = 0; + if (enableEventMessageTrack) { + flags |= FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK; + } + if (enableCea608Track) { + flags |= FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK; + } + extractor = new FragmentedMp4Extractor(flags); } // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. From e86629ef3ae29145f99e488f4e08441f5bb82cfc Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 20 Feb 2017 10:11:19 -0800 Subject: [PATCH 065/140] Allow exposing multiple CEA608 tracks for Transport Streams This CL allows passing multiple formats describing CC channels to the TS payload reader factory. As a simple usecase, ATSC can expose both 608 channels by passing a two element list with the corresponding accessibility channels. The HLS media source can construct this list from the EXT-X-MEDIA:TYPE="CLOSED-CAPTIONS" tags, including language and selection flags. The interface extends without modification to 708. Pending work: * Multiple CC channels in HLS. * caption_service_descriptor parsing for overriding the user's selection. * 708 support in SEI reader. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148030293 --- .../extractor/mp4/FragmentedMp4Extractor.java | 12 +++--- .../ts/DefaultTsPayloadReaderFactory.java | 38 ++++++++++++++++--- .../exoplayer2/extractor/ts/SeiReader.java | 32 +++++++++++++--- .../android/exoplayer2/text/cea/CeaUtil.java | 14 ++++--- 4 files changed, 74 insertions(+), 22 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 6a5662ce10..a228a9b775 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -157,7 +157,7 @@ public final class FragmentedMp4Extractor implements Extractor { // Extractor output. private ExtractorOutput extractorOutput; private TrackOutput eventMessageTrackOutput; - private TrackOutput cea608TrackOutput; + private TrackOutput[] cea608TrackOutputs; // Whether extractorOutput.seekMap has been called. private boolean haveOutputSeekMap; @@ -459,10 +459,12 @@ public final class FragmentedMp4Extractor implements Extractor { eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE)); } - if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutput == null) { - cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1, C.TRACK_TYPE_TEXT); + if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutputs == null) { + TrackOutput cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1, + C.TRACK_TYPE_TEXT); cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null)); + cea608TrackOutputs = new TrackOutput[] {cea608TrackOutput}; } } @@ -1085,7 +1087,7 @@ public final class FragmentedMp4Extractor implements Extractor { output.sampleData(nalStartCode, 4); // Write the NAL unit type byte. output.sampleData(nalPrefix, 1); - processSeiNalUnitPayload = cea608TrackOutput != null + processSeiNalUnitPayload = cea608TrackOutputs != null && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]); sampleBytesWritten += 5; sampleSize += nalUnitLengthFieldLengthDiff; @@ -1103,7 +1105,7 @@ public final class FragmentedMp4Extractor implements Extractor { nalBuffer.setPosition(MimeTypes.VIDEO_H265.equals(track.format.sampleMimeType) ? 1 : 0); nalBuffer.setLimit(unescapedLength); CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalBuffer, - cea608TrackOutput); + cea608TrackOutputs); } else { // Write the payload of the NAL unit. writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 4e94891889..21050d2bbb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -17,9 +17,13 @@ package com.google.android.exoplayer2.extractor.ts; import android.support.annotation.IntDef; import android.util.SparseArray; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; +import com.google.android.exoplayer2.util.MimeTypes; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.List; /** * Default implementation for {@link TsPayloadReader.Factory}. @@ -35,19 +39,36 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact public @interface Flags { } public static final int FLAG_ALLOW_NON_IDR_KEYFRAMES = 1; - public static final int FLAG_IGNORE_AAC_STREAM = 2; - public static final int FLAG_IGNORE_H264_STREAM = 4; - public static final int FLAG_DETECT_ACCESS_UNITS = 8; - public static final int FLAG_IGNORE_SPLICE_INFO_STREAM = 16; + public static final int FLAG_IGNORE_AAC_STREAM = 1 << 1; + public static final int FLAG_IGNORE_H264_STREAM = 1 << 2; + public static final int FLAG_DETECT_ACCESS_UNITS = 1 << 3; + public static final int FLAG_IGNORE_SPLICE_INFO_STREAM = 1 << 4; @Flags private final int flags; + private final List closedCaptionFormats; public DefaultTsPayloadReaderFactory() { this(0); } + /** + * @param flags A combination of {@code FLAG_*} values, which control the behavior of the created + * readers. + */ public DefaultTsPayloadReaderFactory(@Flags int flags) { + this(flags, Collections.singletonList(Format.createTextSampleFormat(null, + MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null))); + } + + /** + * @param flags A combination of {@code FLAG_*} values, which control the behavior of the created + * readers. + * @param closedCaptionFormats {@link Format}s to be exposed by elementary stream readers for + * streams with embedded closed captions. + */ + public DefaultTsPayloadReaderFactory(@Flags int flags, List closedCaptionFormats) { this.flags = flags; + this.closedCaptionFormats = closedCaptionFormats; } @Override @@ -74,10 +95,10 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact return new PesReader(new H262Reader()); case TsExtractor.TS_STREAM_TYPE_H264: return isSet(FLAG_IGNORE_H264_STREAM) ? null - : new PesReader(new H264Reader(new SeiReader(), isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES), + : new PesReader(new H264Reader(buildSeiReader(), isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES), isSet(FLAG_DETECT_ACCESS_UNITS))); case TsExtractor.TS_STREAM_TYPE_H265: - return new PesReader(new H265Reader(new SeiReader())); + return new PesReader(new H265Reader(buildSeiReader())); case TsExtractor.TS_STREAM_TYPE_SPLICE_INFO: return isSet(FLAG_IGNORE_SPLICE_INFO_STREAM) ? null : new SectionReader(new SpliceInfoSectionReader()); @@ -88,6 +109,11 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact } } + private SeiReader buildSeiReader() { + // TODO: Add descriptor parsing to detect channels automatically. + return new SeiReader(closedCaptionFormats); + } + private boolean isSet(@Flags int flag) { return (flags & flag) != 0; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java index a3f4deffcb..1e5d480ea1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java @@ -21,25 +21,45 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.text.cea.CeaUtil; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.List; /** * Consumes SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}. */ /* package */ final class SeiReader { - private TrackOutput output; + private final List closedCaptionFormats; + private final TrackOutput[] outputs; + + /** + * @param closedCaptionFormats A list of formats for the closed caption channels to expose. + */ + public SeiReader(List closedCaptionFormats) { + this.closedCaptionFormats = closedCaptionFormats; + outputs = new TrackOutput[closedCaptionFormats.size()]; + } public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - idGenerator.generateNewId(); - output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); - output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), - MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null)); + for (int i = 0; i < outputs.length; i++) { + idGenerator.generateNewId(); + TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); + Format channelFormat = closedCaptionFormats.get(i); + String channelMimeType = channelFormat.sampleMimeType; + Assertions.checkArgument(MimeTypes.APPLICATION_CEA608.equals(channelMimeType) + || MimeTypes.APPLICATION_CEA708.equals(channelMimeType), + "Invalid closed caption mime type provided: " + channelMimeType); + output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), channelMimeType, null, + Format.NO_VALUE, channelFormat.selectionFlags, channelFormat.language, + channelFormat.accessibilityChannel, null)); + outputs[i] = output; + } } public void consume(long pesTimeUs, ParsableByteArray seiBuffer) { - CeaUtil.consume(pesTimeUs, seiBuffer, output); + CeaUtil.consume(pesTimeUs, seiBuffer, outputs); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java index a39c8c8669..130c7461f9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java @@ -35,14 +35,14 @@ public final class CeaUtil { /** * Consumes the unescaped content of an SEI NAL unit, writing the content of any CEA-608 messages - * as samples to the provided output. + * as samples to all of the provided outputs. * * @param presentationTimeUs The presentation time in microseconds for any samples. * @param seiBuffer The unescaped SEI NAL unit data, excluding the NAL unit start code and type. - * @param output The output to which any samples should be written. + * @param outputs The outputs to which any samples should be written. */ public static void consume(long presentationTimeUs, ParsableByteArray seiBuffer, - TrackOutput output) { + TrackOutput[] outputs) { while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) { int payloadType = readNon255TerminatedValue(seiBuffer); int payloadSize = readNon255TerminatedValue(seiBuffer); @@ -62,8 +62,12 @@ public final class CeaUtil { // Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2) // + cc_data_1 (8) + cc_data_2 (8). int sampleLength = ccCount * 3; - output.sampleData(seiBuffer, sampleLength); - output.sampleMetadata(presentationTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleLength, 0, null); + int sampleStartPosition = seiBuffer.getPosition(); + for (TrackOutput output : outputs) { + seiBuffer.setPosition(sampleStartPosition); + output.sampleData(seiBuffer, sampleLength); + output.sampleMetadata(presentationTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleLength, 0, null); + } // Ignore trailing information in SEI, if any. seiBuffer.skipBytes(payloadSize - (10 + ccCount * 3)); } else { From 896550883f6d183e3ef46351eb3e494df8eaefba Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 22 Feb 2017 04:11:55 -0800 Subject: [PATCH 066/140] Add support for multiple CC channels in HLS Issue:#2161 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148203980 --- .../playlist/HlsMasterPlaylistParserTest.java | 29 ++++++++++++++----- .../exoplayer2/source/hls/HlsChunkSource.java | 10 +++++-- .../exoplayer2/source/hls/HlsMediaChunk.java | 18 ++++++++---- .../exoplayer2/source/hls/HlsMediaPeriod.java | 9 +++--- .../source/hls/HlsSampleStreamWrapper.java | 18 +++--------- .../hls/playlist/HlsMasterPlaylist.java | 9 +++--- .../hls/playlist/HlsPlaylistParser.java | 21 ++++++++++---- 7 files changed, 68 insertions(+), 46 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index f0adf274ee..aa279f23f4 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -19,6 +19,7 @@ import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.util.MimeTypes; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.Charset; @@ -53,12 +54,14 @@ public class HlsMasterPlaylistParserTest extends TestCase { + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + "http://example.com/low.m3u8\n"; - public void testParseMasterPlaylist() throws IOException{ - HlsPlaylist playlist = parsePlaylist(PLAYLIST_URI, MASTER_PLAYLIST); - assertNotNull(playlist); - assertEquals(HlsPlaylist.TYPE_MASTER, playlist.type); + private static final String MASTER_PLAYLIST_WITH_CC = " #EXTM3U \n" + + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + + "http://example.com/low.m3u8\n"; - HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; + public void testParseMasterPlaylist() throws IOException{ + HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST); List variants = masterPlaylist.variants; assertNotNull(variants); @@ -98,18 +101,28 @@ public class HlsMasterPlaylistParserTest extends TestCase { public void testPlaylistWithInvalidHeader() throws IOException { try { - parsePlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER); + parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER); fail("Expected exception not thrown."); } catch (ParserException e) { // Expected due to invalid header. } } - private static HlsPlaylist parsePlaylist(String uri, String playlistString) throws IOException { + public void testPlaylistWithClosedCaption() throws IOException { + HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST_WITH_CC); + assertEquals(1, playlist.muxedCaptionFormats.size()); + Format closedCaptionFormat = playlist.muxedCaptionFormats.get(0); + assertEquals(MimeTypes.APPLICATION_CEA708, closedCaptionFormat.sampleMimeType); + assertEquals(4, closedCaptionFormat.accessibilityChannel); + assertEquals("es", closedCaptionFormat.language); + } + + private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) + throws IOException { Uri playlistUri = Uri.parse(uri); ByteArrayInputStream inputStream = new ByteArrayInputStream( playlistString.getBytes(Charset.forName(C.UTF8_NAME))); - return new HlsPlaylistParser().parse(playlistUri, inputStream); + return (HlsMasterPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index c7c66fbd61..7ba5cf2df1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -38,6 +38,7 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.math.BigInteger; import java.util.Arrays; +import java.util.List; import java.util.Locale; /** @@ -85,6 +86,7 @@ import java.util.Locale; private final HlsUrl[] variants; private final HlsPlaylistTracker playlistTracker; private final TrackGroup trackGroup; + private final List muxedCaptionFormats; private boolean isTimestampMaster; private byte[] scratchSpace; @@ -107,14 +109,16 @@ import java.util.Locale; * @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the * same provider. + * @param muxedCaptionFormats List of muxed caption {@link Format}s. */ public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsUrl[] variants, - DataSource dataSource, TimestampAdjusterProvider timestampAdjusterProvider) { + DataSource dataSource, TimestampAdjusterProvider timestampAdjusterProvider, + List muxedCaptionFormats) { this.playlistTracker = playlistTracker; this.variants = variants; this.dataSource = dataSource; this.timestampAdjusterProvider = timestampAdjusterProvider; - + this.muxedCaptionFormats = muxedCaptionFormats; Format[] variantFormats = new Format[variants.length]; int[] initialTrackSelection = new int[variants.length]; for (int i = 0; i < variants.length; i++) { @@ -282,7 +286,7 @@ import java.util.Locale; DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, null); out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, selectedUrl, - trackSelection.getSelectionReason(), trackSelection.getSelectionData(), + muxedCaptionFormats, trackSelection.getSelectionReason(), trackSelection.getSelectionData(), startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, discontinuitySequence, isTimestampMaster, timestampAdjuster, previous, encryptionKey, encryptionIv); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 5885797896..357a32f086 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls; import android.text.TextUtils; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; @@ -39,6 +40,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; /** @@ -84,6 +86,7 @@ import java.util.concurrent.atomic.AtomicInteger; private final Extractor previousExtractor; private final boolean shouldSpliceIn; private final boolean needNewExtractor; + private final List muxedCaptionFormats; private final boolean isPackedAudio; private final Id3Decoder id3Decoder; @@ -102,6 +105,7 @@ import java.util.concurrent.atomic.AtomicInteger; * @param dataSpec Defines the data to be loaded. * @param initDataSpec Defines the initialization data to be fed to new extractors. May be null. * @param hlsUrl The url of the playlist from which this chunk was obtained. + * @param muxedCaptionFormats List of muxed caption {@link Format}s. * @param trackSelectionReason See {@link #trackSelectionReason}. * @param trackSelectionData See {@link #trackSelectionData}. * @param startTimeUs The start time of the chunk in microseconds. @@ -115,17 +119,19 @@ import java.util.concurrent.atomic.AtomicInteger; * @param encryptionIv For AES encryption chunks, the encryption initialization vector. */ public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec, - HlsUrl hlsUrl, int trackSelectionReason, Object trackSelectionData, long startTimeUs, - long endTimeUs, int chunkIndex, int discontinuitySequenceNumber, - boolean isMasterTimestampSource, TimestampAdjuster timestampAdjuster, - HlsMediaChunk previousChunk, byte[] encryptionKey, byte[] encryptionIv) { + HlsUrl hlsUrl, List muxedCaptionFormats, int trackSelectionReason, + Object trackSelectionData, long startTimeUs, long endTimeUs, int chunkIndex, + int discontinuitySequenceNumber, boolean isMasterTimestampSource, + TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, byte[] encryptionKey, + byte[] encryptionIv) { super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, hlsUrl.format, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); + this.discontinuitySequenceNumber = discontinuitySequenceNumber; this.initDataSpec = initDataSpec; this.hlsUrl = hlsUrl; + this.muxedCaptionFormats = muxedCaptionFormats; this.isMasterTimestampSource = isMasterTimestampSource; this.timestampAdjuster = timestampAdjuster; - this.discontinuitySequenceNumber = discontinuitySequenceNumber; // Note: this.dataSource and dataSource may be different. this.isEncrypted = this.dataSource instanceof Aes128DataSource; lastPathSegment = dataSpec.uri.getLastPathSegment(); @@ -363,7 +369,7 @@ import java.util.concurrent.atomic.AtomicInteger; } } extractor = new TsExtractor(timestampAdjuster, - new DefaultTsPayloadReaderFactory(esReaderFactoryFlags), true); + new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, muxedCaptionFormats), true); } if (usingNewExtractor) { extractor.init(extractorOutput); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 6082372b05..0ae8becfc0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -317,7 +317,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[selectedVariants.size()]; selectedVariants.toArray(variants); HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, - variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat); + variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormats); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrapper.setIsTimestampMaster(true); sampleStreamWrapper.continuePreparing(); @@ -343,13 +343,12 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants, - Format muxedAudioFormat, Format muxedCaptionFormat) { + Format muxedAudioFormat, List muxedCaptionFormats) { DataSource dataSource = dataSourceFactory.createDataSource(); HlsChunkSource defaultChunkSource = new HlsChunkSource(playlistTracker, variants, dataSource, - timestampAdjusterProvider); + timestampAdjusterProvider, muxedCaptionFormats); return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, - preparePositionUs, muxedAudioFormat, muxedCaptionFormat, minLoadableRetryCount, - eventDispatcher); + preparePositionUs, muxedAudioFormat, minLoadableRetryCount, eventDispatcher); } private void continuePreparingOrLoading() { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 6980fdd7a4..0e3ee6fa9c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -77,7 +77,6 @@ import java.util.LinkedList; private final HlsChunkSource chunkSource; private final Allocator allocator; private final Format muxedAudioFormat; - private final Format muxedCaptionFormat; private final int minLoadableRetryCount; private final Loader loader; private final EventDispatcher eventDispatcher; @@ -113,21 +112,18 @@ import java.util.LinkedList; * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param positionUs The position from which to start loading media. * @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the master playlist. - * @param muxedCaptionFormat Optional muxed closed caption {@link Format} as defined by the master - * playlist. * @param minLoadableRetryCount The minimum number of times that the source should retry a load * before propagating an error. * @param eventDispatcher A dispatcher to notify of events. */ public HlsSampleStreamWrapper(int trackType, Callback callback, HlsChunkSource chunkSource, - Allocator allocator, long positionUs, Format muxedAudioFormat, Format muxedCaptionFormat, - int minLoadableRetryCount, EventDispatcher eventDispatcher) { + Allocator allocator, long positionUs, Format muxedAudioFormat, int minLoadableRetryCount, + EventDispatcher eventDispatcher) { this.trackType = trackType; this.callback = callback; this.chunkSource = chunkSource; this.allocator = allocator; this.muxedAudioFormat = muxedAudioFormat; - this.muxedCaptionFormat = muxedCaptionFormat; this.minLoadableRetryCount = minLoadableRetryCount; this.eventDispatcher = eventDispatcher; loader = new Loader("Loader:HlsSampleStreamWrapper"); @@ -589,14 +585,8 @@ import java.util.LinkedList; trackGroups[i] = new TrackGroup(formats); primaryTrackGroupIndex = i; } else { - Format trackFormat = null; - if (primaryExtractorTrackType == PRIMARY_TYPE_VIDEO) { - if (MimeTypes.isAudio(sampleFormat.sampleMimeType)) { - trackFormat = muxedAudioFormat; - } else if (MimeTypes.APPLICATION_CEA608.equals(sampleFormat.sampleMimeType)) { - trackFormat = muxedCaptionFormat; - } - } + Format trackFormat = primaryExtractorTrackType == PRIMARY_TYPE_VIDEO + && MimeTypes.isAudio(sampleFormat.sampleMimeType) ? muxedAudioFormat : null; trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat)); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java index ab18fda2f0..5580017805 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java @@ -52,22 +52,23 @@ public final class HlsMasterPlaylist extends HlsPlaylist { public final List subtitles; public final Format muxedAudioFormat; - public final Format muxedCaptionFormat; + public final List muxedCaptionFormats; public HlsMasterPlaylist(String baseUri, List variants, List audios, - List subtitles, Format muxedAudioFormat, Format muxedCaptionFormat) { + List subtitles, Format muxedAudioFormat, List muxedCaptionFormats) { super(baseUri, HlsPlaylist.TYPE_MASTER); this.variants = Collections.unmodifiableList(variants); this.audios = Collections.unmodifiableList(audios); this.subtitles = Collections.unmodifiableList(subtitles); this.muxedAudioFormat = muxedAudioFormat; - this.muxedCaptionFormat = muxedCaptionFormat; + this.muxedCaptionFormats = Collections.unmodifiableList(muxedCaptionFormats); } public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUri) { List variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUri)); List emptyList = Collections.emptyList(); - return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, null); + return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, + Collections.emptyList()); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 6efd1fecb2..6c29535326 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -94,7 +94,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser audios = new ArrayList<>(); ArrayList subtitles = new ArrayList<>(); Format muxedAudioFormat = null; - Format muxedCaptionFormat = null; + ArrayList muxedCaptionFormats = new ArrayList<>(); String line; while (iterator.hasNext()) { @@ -198,10 +199,18 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Wed, 22 Feb 2017 06:25:29 -0800 Subject: [PATCH 067/140] Separate input/output handling in BufferProcessors. This allows BufferProcessors to partially and/or asynchronously handle input/output. Document contract for queueInput and getOutput. Update ResamplingBufferProcessor to use the new interface. Separate submitting bytes vs. writing data to the AudioTrack. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148212269 --- .../android/exoplayer2/audio/AudioTrack.java | 207 +++++++++++++----- .../exoplayer2/audio/BufferProcessor.java | 48 +++- .../audio/ResamplingBufferProcessor.java | 80 ++++--- .../google/android/exoplayer2/util/Util.java | 22 ++ 4 files changed, 265 insertions(+), 92 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index cc3f91bc0a..38085dfc3a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.util.Util; import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.ArrayList; /** * Plays audio data. The implementation delegates to an {@link android.media.AudioTrack} and handles @@ -269,7 +270,7 @@ public final class AudioTrack { public static boolean failOnSpuriousAudioTimestamp = false; private final AudioCapabilities audioCapabilities; - private final BufferProcessor[] bufferProcessors; + private final BufferProcessor[] availableBufferProcessors; private final Listener listener; private final ConditionVariable releasingConditionVariable; private final long[] playheadOffsets; @@ -290,7 +291,6 @@ public final class AudioTrack { @C.StreamType private int streamType; private boolean passthrough; - private int pcmFrameSize; private int bufferSize; private long bufferSizeUs; @@ -305,8 +305,12 @@ public final class AudioTrack { private long lastTimestampSampleTimeUs; private Method getLatencyMethod; + private int pcmFrameSize; private long submittedPcmBytes; private long submittedEncodedFrames; + private int outputPcmFrameSize; + private long writtenPcmBytes; + private long writtenEncodedFrames; private int framesPerEncodedSample; private int startMediaTimeState; private long startMediaTimeUs; @@ -314,6 +318,8 @@ public final class AudioTrack { private long latencyUs; private float volume; + private BufferProcessor[] bufferProcessors; + private ByteBuffer[] outputBuffers; private ByteBuffer inputBuffer; private ByteBuffer outputBuffer; private byte[] preV21OutputBuffer; @@ -335,9 +341,9 @@ public final class AudioTrack { public AudioTrack(AudioCapabilities audioCapabilities, BufferProcessor[] bufferProcessors, Listener listener) { this.audioCapabilities = audioCapabilities; - this.bufferProcessors = new BufferProcessor[bufferProcessors.length + 1]; - this.bufferProcessors[0] = new ResamplingBufferProcessor(); - System.arraycopy(bufferProcessors, 0, this.bufferProcessors, 1, bufferProcessors.length); + availableBufferProcessors = new BufferProcessor[bufferProcessors.length + 1]; + availableBufferProcessors[0] = new ResamplingBufferProcessor(); + System.arraycopy(bufferProcessors, 0, availableBufferProcessors, 1, bufferProcessors.length); this.listener = listener; releasingConditionVariable = new ConditionVariable(true); if (Util.SDK_INT >= 18) { @@ -360,6 +366,8 @@ public final class AudioTrack { startMediaTimeState = START_NOT_SET; streamType = C.STREAM_TYPE_DEFAULT; audioSessionId = C.AUDIO_SESSION_ID_UNSET; + this.bufferProcessors = new BufferProcessor[0]; + outputBuffers = new ByteBuffer[0]; } /** @@ -440,14 +448,39 @@ public final class AudioTrack { @C.PcmEncoding int pcmEncoding, int specifiedBufferSize) throws ConfigurationException { boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); @C.Encoding int encoding = passthrough ? getEncodingForMimeType(mimeType) : pcmEncoding; + boolean flush = false; if (!passthrough) { - for (BufferProcessor bufferProcessor : bufferProcessors) { + pcmFrameSize = Util.getPcmFrameSize(pcmEncoding, channelCount); + + // Reconfigure the buffer processors. + ArrayList newBufferProcessors = new ArrayList<>(); + for (BufferProcessor bufferProcessor : availableBufferProcessors) { + boolean wasActive = bufferProcessor.isActive(); try { - bufferProcessor.configure(sampleRate, channelCount, encoding); + flush |= bufferProcessor.configure(sampleRate, channelCount, encoding); } catch (BufferProcessor.UnhandledFormatException e) { throw new ConfigurationException(e); } - encoding = bufferProcessor.getOutputEncoding(); + boolean isActive = bufferProcessor.isActive(); + flush |= isActive != wasActive; + if (isActive) { + newBufferProcessors.add(bufferProcessor); + channelCount = bufferProcessor.getOutputChannelCount(); + encoding = bufferProcessor.getOutputEncoding(); + } else { + bufferProcessor.flush(); + } + } + + if (flush) { + int count = newBufferProcessors.size(); + bufferProcessors = newBufferProcessors.toArray(new BufferProcessor[count]); + outputBuffers = new ByteBuffer[count]; + for (int i = 0; i < count; i++) { + BufferProcessor bufferProcessor = bufferProcessors[i]; + bufferProcessor.flush(); + outputBuffers[i] = bufferProcessor.getOutput(); + } } } @@ -502,7 +535,7 @@ public final class AudioTrack { channelConfig = AudioFormat.CHANNEL_OUT_STEREO; } - if (isInitialized() && this.encoding == encoding && this.sampleRate == sampleRate + if (!flush && isInitialized() && this.encoding == encoding && this.sampleRate == sampleRate && this.channelConfig == channelConfig) { // We already have an audio track with the correct sample rate, channel config and encoding. return; @@ -514,8 +547,8 @@ public final class AudioTrack { this.passthrough = passthrough; this.sampleRate = sampleRate; this.channelConfig = channelConfig; - pcmFrameSize = 2 * channelCount; // 2 bytes per 16-bit sample * number of channels. outputEncoding = passthrough ? encoding : C.ENCODING_PCM_16BIT; + outputPcmFrameSize = Util.getPcmFrameSize(C.ENCODING_PCM_16BIT, channelCount); if (specifiedBufferSize != 0) { bufferSize = specifiedBufferSize; @@ -534,14 +567,14 @@ public final class AudioTrack { android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding); Assertions.checkState(minBufferSize != ERROR_BAD_VALUE); int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR; - int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * pcmFrameSize; + int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize; int maxAppBufferSize = (int) Math.max(minBufferSize, - durationUsToFrames(MAX_BUFFER_DURATION_US) * pcmFrameSize); + durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize); bufferSize = multipliedBufferSize < minAppBufferSize ? minAppBufferSize : multipliedBufferSize > maxAppBufferSize ? maxAppBufferSize : multipliedBufferSize; } - bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(pcmBytesToFrames(bufferSize)); + bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(bufferSize / outputPcmFrameSize); } private void initialize() throws InitializationException { @@ -616,20 +649,19 @@ public final class AudioTrack { } /** - * Attempts to write data from a {@link ByteBuffer} to the audio track, starting from its current - * position and ending at its limit (exclusive). The position of the {@link ByteBuffer} is - * advanced by the number of bytes that were successfully written. - * {@link Listener#onPositionDiscontinuity()} will be called if {@code presentationTimeUs} is - * discontinuous with the last buffer handled since the track was reset. + * Attempts to process data from a {@link ByteBuffer}, starting from its current position and + * ending at its limit (exclusive). The position of the {@link ByteBuffer} is advanced by the + * number of bytes that were handled. {@link Listener#onPositionDiscontinuity()} will be called if + * {@code presentationTimeUs} is discontinuous with the last buffer handled since the last reset. *

    - * Returns whether the data was written in full. If the data was not written in full then the same + * Returns whether the data was handled in full. If the data was not handled in full then the same * {@link ByteBuffer} must be provided to subsequent calls until it has been fully consumed, * except in the case of an interleaving call to {@link #reset()} (or an interleaving call to * {@link #configure(String, int, int, int, int)} that caused the track to be reset). * - * @param buffer The buffer containing audio data to play back. - * @param presentationTimeUs Presentation timestamp of the next buffer in microseconds. - * @return Whether the buffer was consumed fully. + * @param buffer The buffer containing audio data. + * @param presentationTimeUs The presentation timestamp of the buffer in microseconds. + * @return Whether the buffer was handled fully. * @throws InitializationException If an error occurs initializing the track. * @throws WriteException If an error occurs writing the audio data. */ @@ -703,53 +735,100 @@ public final class AudioTrack { } } + if (passthrough) { + submittedEncodedFrames += framesPerEncodedSample; + } else { + submittedPcmBytes += buffer.remaining(); + } + inputBuffer = buffer; - if (!passthrough) { - for (BufferProcessor bufferProcessor : bufferProcessors) { - buffer = bufferProcessor.handleBuffer(buffer); - } - } - outputBuffer = buffer; - if (Util.SDK_INT < 21) { - int bytesRemaining = outputBuffer.remaining(); - if (preV21OutputBuffer == null || preV21OutputBuffer.length < bytesRemaining) { - preV21OutputBuffer = new byte[bytesRemaining]; - } - int originalPosition = outputBuffer.position(); - outputBuffer.get(preV21OutputBuffer, 0, bytesRemaining); - outputBuffer.position(originalPosition); - preV21OutputBufferOffset = 0; - } } - if (writeOutputBuffer(presentationTimeUs)) { + if (passthrough) { + // Passthrough buffers are not processed. + writeBuffer(inputBuffer, presentationTimeUs); + } else { + processBuffers(presentationTimeUs); + } + + if (!inputBuffer.hasRemaining()) { inputBuffer = null; return true; } return false; } - private boolean writeOutputBuffer(long presentationTimeUs) throws WriteException { - int bytesRemaining = outputBuffer.remaining(); + private void processBuffers(long avSyncPresentationTimeUs) throws WriteException { + int count = bufferProcessors.length; + int index = count; + while (index >= 0) { + ByteBuffer input = index > 0 ? outputBuffers[index - 1] : inputBuffer; + if (index == count) { + writeBuffer(input, avSyncPresentationTimeUs); + } else { + BufferProcessor bufferProcessor = bufferProcessors[index]; + bufferProcessor.queueInput(input); + ByteBuffer output = bufferProcessor.getOutput(); + outputBuffers[index] = output; + if (output.hasRemaining()) { + // Handle the output as input to the next buffer processor or the AudioTrack. + index++; + continue; + } + } + + if (input.hasRemaining()) { + // The input wasn't consumed and no output was produced, so give up for now. + return; + } + + // Get more input from upstream. + index--; + } + } + + @SuppressWarnings("ReferenceEquality") + private boolean writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) + throws WriteException { + if (!buffer.hasRemaining()) { + return true; + } + if (outputBuffer != null) { + Assertions.checkArgument(outputBuffer == buffer); + } else { + outputBuffer = buffer; + if (Util.SDK_INT < 21) { + int bytesRemaining = buffer.remaining(); + if (preV21OutputBuffer == null || preV21OutputBuffer.length < bytesRemaining) { + preV21OutputBuffer = new byte[bytesRemaining]; + } + int originalPosition = buffer.position(); + buffer.get(preV21OutputBuffer, 0, bytesRemaining); + buffer.position(originalPosition); + preV21OutputBufferOffset = 0; + } + } + int bytesRemaining = buffer.remaining(); int bytesWritten = 0; if (Util.SDK_INT < 21) { // passthrough == false // Work out how many bytes we can write without the risk of blocking. int bytesPending = - (int) (submittedPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * pcmFrameSize)); + (int) (writtenPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * outputPcmFrameSize)); int bytesToWrite = bufferSize - bytesPending; if (bytesToWrite > 0) { bytesToWrite = Math.min(bytesRemaining, bytesToWrite); bytesWritten = audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite); if (bytesWritten > 0) { preV21OutputBufferOffset += bytesWritten; - outputBuffer.position(outputBuffer.position() + bytesWritten); + buffer.position(buffer.position() + bytesWritten); } } } else if (tunneling) { - bytesWritten = writeNonBlockingWithAvSyncV21(audioTrack, outputBuffer, bytesRemaining, - presentationTimeUs); + Assertions.checkState(avSyncPresentationTimeUs != C.TIME_UNSET); + bytesWritten = writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, + avSyncPresentationTimeUs); } else { - bytesWritten = writeNonBlockingV21(audioTrack, outputBuffer, bytesRemaining); + bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining); } lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); @@ -759,12 +838,13 @@ public final class AudioTrack { } if (!passthrough) { - submittedPcmBytes += bytesWritten; + writtenPcmBytes += bytesWritten; } if (bytesWritten == bytesRemaining) { if (passthrough) { - submittedEncodedFrames += framesPerEncodedSample; + writtenEncodedFrames += framesPerEncodedSample; } + outputBuffer = null; return true; } return false; @@ -775,7 +855,8 @@ public final class AudioTrack { */ public void handleEndOfStream() { if (isInitialized()) { - audioTrackUtil.handleEndOfStream(getSubmittedFrames()); + // TODO: Drain buffer processors before stopping the AudioTrack. + audioTrackUtil.handleEndOfStream(getWrittenFrames()); bytesUntilNextAvSync = 0; } } @@ -785,7 +866,7 @@ public final class AudioTrack { */ public boolean hasPendingData() { return isInitialized() - && (getSubmittedFrames() > audioTrackUtil.getPlaybackHeadPosition() + && (getWrittenFrames() > audioTrackUtil.getPlaybackHeadPosition() || overrideHasPendingData()); } @@ -838,6 +919,11 @@ public final class AudioTrack { /** * Enables tunneling. The audio track is reset if tunneling was previously disabled or if the * audio session id has changed. Enabling tunneling requires platform API version 21 onwards. + *

    + * If this instance has {@link BufferProcessor}s and tunneling is enabled, care must be taken that + * buffer processors do not output buffers with a different duration than their input, and buffer + * processors must produce output corresponding to their last input immediately after that input + * is queued. * * @param tunnelingAudioSessionId The audio session id to use. * @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21. @@ -907,12 +993,17 @@ public final class AudioTrack { if (isInitialized()) { submittedPcmBytes = 0; submittedEncodedFrames = 0; + writtenPcmBytes = 0; + writtenEncodedFrames = 0; framesPerEncodedSample = 0; inputBuffer = null; - avSyncHeader = null; - for (BufferProcessor bufferProcessor : bufferProcessors) { + outputBuffer = null; + for (int i = 0; i < bufferProcessors.length; i++) { + BufferProcessor bufferProcessor = bufferProcessors[i]; bufferProcessor.flush(); + outputBuffers[i] = bufferProcessor.getOutput(); } + avSyncHeader = null; bytesUntilNextAvSync = 0; startMediaTimeState = START_NOT_SET; latencyUs = 0; @@ -946,7 +1037,7 @@ public final class AudioTrack { public void release() { reset(); releaseKeepSessionIdAudioTrack(); - for (BufferProcessor bufferProcessor : bufferProcessors) { + for (BufferProcessor bufferProcessor : availableBufferProcessors) { bufferProcessor.release(); } audioSessionId = C.AUDIO_SESSION_ID_UNSET; @@ -1092,10 +1183,6 @@ public final class AudioTrack { return audioTrack != null; } - private long pcmBytesToFrames(long byteCount) { - return byteCount / pcmFrameSize; - } - private long framesToDurationUs(long frameCount) { return (frameCount * C.MICROS_PER_SECOND) / sampleRate; } @@ -1105,7 +1192,11 @@ public final class AudioTrack { } private long getSubmittedFrames() { - return passthrough ? submittedEncodedFrames : pcmBytesToFrames(submittedPcmBytes); + return passthrough ? submittedEncodedFrames : (submittedPcmBytes / pcmFrameSize); + } + + private long getWrittenFrames() { + return passthrough ? writtenEncodedFrames : (writtenPcmBytes / outputPcmFrameSize); } private void resetSyncParams() { diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java index 4f604f1a5d..0a58785ef2 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.audio; import com.google.android.exoplayer2.C; import java.nio.ByteBuffer; +import java.nio.ByteOrder; /** * Interface for processors of audio buffers. @@ -36,30 +37,61 @@ public interface BufferProcessor { } /** - * Configures this processor to take input buffers with the specified format. + * An empty, direct {@link ByteBuffer}. + */ + ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder()); + + /** + * Configures the processor to process input buffers with the specified format and returns whether + * the processor must be flushed. After calling this method, {@link #isActive()} returns whether + * the processor needs to handle buffers; if not, the processor will not accept any buffers until + * it is reconfigured. {@link #getOutputChannelCount()} and {@link #getOutputEncoding()} return + * the processor's output format. * * @param sampleRateHz The sample rate of input audio in Hz. * @param channelCount The number of interleaved channels in input audio. * @param encoding The encoding of input audio. + * @return Whether the processor must be flushed. * @throws UnhandledFormatException Thrown if the specified format can't be handled as input. */ - void configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) + boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) throws UnhandledFormatException; /** - * Returns the encoding used in buffers output by this processor. + * Returns whether the processor is configured and active. + */ + boolean isActive(); + + /** + * Returns the number of audio channels in the data output by the processor. + */ + int getOutputChannelCount(); + + /** + * Returns the audio encoding used in the data output by the processor. */ @C.Encoding int getOutputEncoding(); /** - * Processes the data in the specified input buffer in its entirety. + * Queues audio data between the position and limit of the input {@code buffer} for processing. + * {@code buffer} must be a direct byte buffer with native byte order. Its contents are treated as + * read-only. Its position will be advanced by the number of bytes consumed (which may be zero). + * The caller retains ownership of the provided buffer. Calling this method invalidates any + * previous buffer returned by {@link #getOutput()}. * - * @param input A buffer containing the input data to process. - * @return A buffer containing the processed output. This may be the same as the input buffer if - * no processing was required. + * @param buffer The input buffer to process. */ - ByteBuffer handleBuffer(ByteBuffer input); + void queueInput(ByteBuffer buffer); + + /** + * Returns a buffer containing processed output data between its position and limit. The buffer + * will always be a direct byte buffer with native byte order. Calling this method invalidates any + * previously returned buffer. The buffer will be empty if no output is available. + * + * @return A buffer containing processed output data between its position and limit. + */ + ByteBuffer getOutput(); /** * Clears any state in preparation for receiving a new stream of buffers. diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java index 507cdbcdd1..14bd58c3d8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java @@ -18,31 +18,55 @@ package com.google.android.exoplayer2.audio; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import java.nio.ByteBuffer; +import java.nio.ByteOrder; /** - * A {@link BufferProcessor} that outputs buffers in {@link C#ENCODING_PCM_16BIT}. + * A {@link BufferProcessor} that converts audio data to {@link C#ENCODING_PCM_16BIT}. */ /* package */ final class ResamplingBufferProcessor implements BufferProcessor { + private int channelCount; @C.PcmEncoding private int encoding; + private ByteBuffer buffer; private ByteBuffer outputBuffer; + /** + * Creates a new buffer processor that converts audio data to {@link C#ENCODING_PCM_16BIT}. + */ public ResamplingBufferProcessor() { encoding = C.ENCODING_INVALID; + buffer = EMPTY_BUFFER; + outputBuffer = EMPTY_BUFFER; } @Override - public void configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) + public boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) throws UnhandledFormatException { if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT && encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) { throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); } - if (encoding == C.ENCODING_PCM_16BIT) { - outputBuffer = null; + this.channelCount = channelCount; + if (this.encoding == encoding) { + return false; } + this.encoding = encoding; + if (encoding == C.ENCODING_PCM_16BIT) { + buffer = EMPTY_BUFFER; + } + return true; + } + + @Override + public boolean isActive() { + return encoding != C.ENCODING_INVALID && encoding != C.ENCODING_PCM_16BIT; + } + + @Override + public int getOutputChannelCount() { + return channelCount; } @Override @@ -51,16 +75,13 @@ import java.nio.ByteBuffer; } @Override - public ByteBuffer handleBuffer(ByteBuffer buffer) { - int position = buffer.position(); - int limit = buffer.limit(); + public void queueInput(ByteBuffer inputBuffer) { + // Prepare the output buffer. + int position = inputBuffer.position(); + int limit = inputBuffer.limit(); int size = limit - position; - int resampledSize; switch (encoding) { - case C.ENCODING_PCM_16BIT: - // No processing required. - return buffer; case C.ENCODING_PCM_8BIT: resampledSize = size * 2; break; @@ -70,40 +91,39 @@ import java.nio.ByteBuffer; case C.ENCODING_PCM_32BIT: resampledSize = size / 2; break; + case C.ENCODING_PCM_16BIT: case C.ENCODING_INVALID: case Format.NO_VALUE: default: - // Never happens. throw new IllegalStateException(); } - - if (outputBuffer == null || outputBuffer.capacity() < resampledSize) { - outputBuffer = ByteBuffer.allocateDirect(resampledSize).order(buffer.order()); + if (buffer.capacity() < resampledSize) { + buffer = ByteBuffer.allocateDirect(resampledSize).order(ByteOrder.nativeOrder()); } else { - outputBuffer.clear(); + buffer.clear(); } - // Samples are little endian. + // Resample the little endian input and update the input/output buffers. switch (encoding) { case C.ENCODING_PCM_8BIT: // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. for (int i = position; i < limit; i++) { - outputBuffer.put((byte) 0); - outputBuffer.put((byte) ((buffer.get(i) & 0xFF) - 128)); + buffer.put((byte) 0); + buffer.put((byte) ((inputBuffer.get(i) & 0xFF) - 128)); } break; case C.ENCODING_PCM_24BIT: // 24->16 bit resampling. Drop the least significant byte. for (int i = position; i < limit; i += 3) { - outputBuffer.put(buffer.get(i + 1)); - outputBuffer.put(buffer.get(i + 2)); + buffer.put(inputBuffer.get(i + 1)); + buffer.put(inputBuffer.get(i + 2)); } break; case C.ENCODING_PCM_32BIT: // 32->16 bit resampling. Drop the two least significant bytes. for (int i = position; i < limit; i += 4) { - outputBuffer.put(buffer.get(i + 2)); - outputBuffer.put(buffer.get(i + 3)); + buffer.put(inputBuffer.get(i + 2)); + buffer.put(inputBuffer.get(i + 3)); } break; case C.ENCODING_PCM_16BIT: @@ -113,19 +133,27 @@ import java.nio.ByteBuffer; // Never happens. throw new IllegalStateException(); } + inputBuffer.position(inputBuffer.limit()); + buffer.flip(); + outputBuffer = buffer; + } - outputBuffer.flip(); + @Override + public ByteBuffer getOutput() { + ByteBuffer outputBuffer = this.outputBuffer; + this.outputBuffer = EMPTY_BUFFER; return outputBuffer; } @Override public void flush() { - // Do nothing. + outputBuffer = EMPTY_BUFFER; } @Override public void release() { - outputBuffer = null; + buffer = EMPTY_BUFFER; + outputBuffer = EMPTY_BUFFER; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/src/main/java/com/google/android/exoplayer2/util/Util.java index e854c05165..f4ba21152a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -767,6 +767,28 @@ public final class Util { } } + /** + * Returns the frame size for audio with {@code channelCount} channels in the specified encoding. + * + * @param pcmEncoding The encoding of the audio data. + * @param channelCount The channel count. + * @return The size of one audio frame in bytes. + */ + public static int getPcmFrameSize(@C.PcmEncoding int pcmEncoding, int channelCount) { + switch (pcmEncoding) { + case C.ENCODING_PCM_8BIT: + return channelCount; + case C.ENCODING_PCM_16BIT: + return channelCount * 2; + case C.ENCODING_PCM_24BIT: + return channelCount * 3; + case C.ENCODING_PCM_32BIT: + return channelCount * 4; + default: + throw new IllegalArgumentException(); + } + } + /** * Makes a best guess to infer the type from a file name. * From ddbced73174404bf6c177c03d5927fad72e1d031 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 Feb 2017 07:21:49 -0800 Subject: [PATCH 068/140] Conditionally enable EMSG/608 based on manifest declarations Issue: #2362 Issue: #2176 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148216614 --- .../source/dash/DashMediaPeriod.java | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index c1d52c7a77..cab956ccfe 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.Period; import com.google.android.exoplayer2.source.dash.manifest.Representation; +import com.google.android.exoplayer2.source.dash.manifest.SchemeValuePair; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; @@ -201,13 +202,59 @@ import java.util.List; long positionUs) { int adaptationSetIndex = trackGroups.indexOf(selection.getTrackGroup()); AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex); + boolean enableEventMessageTrack = hasEventMessageTrack(adaptationSet); + boolean enableCea608Track = hasCea608Track(adaptationSet); DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource( manifestLoaderErrorThrower, manifest, index, adaptationSetIndex, selection, - elapsedRealtimeOffset, false, false); + elapsedRealtimeOffset, enableEventMessageTrack, enableCea608Track); return new ChunkSampleStream<>(adaptationSet.type, chunkSource, this, allocator, positionUs, minLoadableRetryCount, eventDispatcher); } + private static int getEventMessageTrackCount(Period period) { + List adaptationSets = period.adaptationSets; + int inbandEventStreamTrackCount = 0; + for (int i = 0; i < adaptationSets.size(); i++) { + if (hasEventMessageTrack(adaptationSets.get(i))) { + inbandEventStreamTrackCount++; + } + } + return inbandEventStreamTrackCount; + } + + private static boolean hasEventMessageTrack(AdaptationSet adaptationSet) { + List representations = adaptationSet.representations; + for (int i = 0; i < representations.size(); i++) { + Representation representation = representations.get(i); + if (!representation.inbandEventStreams.isEmpty()) { + return true; + } + } + return false; + } + + private static int getCea608TrackCount(Period period) { + List adaptationSets = period.adaptationSets; + int cea608TrackCount = 0; + for (int i = 0; i < adaptationSets.size(); i++) { + if (hasCea608Track(adaptationSets.get(i))) { + cea608TrackCount++; + } + } + return cea608TrackCount; + } + + private static boolean hasCea608Track(AdaptationSet adaptationSet) { + List descriptors = adaptationSet.accessibilityDescriptors; + for (int i = 0; i < descriptors.size(); i++) { + SchemeValuePair descriptor = descriptors.get(i); + if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri)) { + return true; + } + } + return false; + } + @SuppressWarnings("unchecked") private static ChunkSampleStream[] newSampleStreamArray(int length) { return new ChunkSampleStream[length]; From 698e081edab82c4293de0ada171d6bcb3eacd3fc Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 Feb 2017 07:39:05 -0800 Subject: [PATCH 069/140] Handle empty PRIV frames Issue: #2486 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148217936 --- .../google/android/exoplayer2/metadata/id3/Id3Decoder.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index 16059ccfbf..d5f5b08370 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -404,6 +404,11 @@ public final class Id3Decoder implements MetadataDecoder { private static PrivFrame decodePrivFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { + if (frameSize == 0) { + // Frame is empty. + return new PrivFrame("", new byte[0]); + } + byte[] data = new byte[frameSize]; id3Data.readBytes(data, 0, frameSize); From 3fc3349e9535d8763f9a4c34c1d9a9e2ce7024a3 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 23 Feb 2017 01:55:56 -0800 Subject: [PATCH 070/140] Add support for Caption Format Descriptor This allows the TsExtractor to automatically determine the closed caption tracks to expose by parsing available descriptors. Issue:#2161 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148321380 --- .../ts/DefaultTsPayloadReaderFactory.java | 85 +++++++++++++++---- .../exoplayer2/source/hls/HlsMediaChunk.java | 4 + 2 files changed, 71 insertions(+), 18 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 21050d2bbb..587f036797 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -20,8 +20,10 @@ import android.util.SparseArray; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.ParsableByteArray; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -35,7 +37,8 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = {FLAG_ALLOW_NON_IDR_KEYFRAMES, FLAG_IGNORE_AAC_STREAM, - FLAG_IGNORE_H264_STREAM, FLAG_DETECT_ACCESS_UNITS, FLAG_IGNORE_SPLICE_INFO_STREAM}) + FLAG_IGNORE_H264_STREAM, FLAG_DETECT_ACCESS_UNITS, FLAG_IGNORE_SPLICE_INFO_STREAM, + FLAG_OVERRIDE_CAPTION_DESCRIPTORS}) public @interface Flags { } public static final int FLAG_ALLOW_NON_IDR_KEYFRAMES = 1; @@ -43,31 +46,33 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact public static final int FLAG_IGNORE_H264_STREAM = 1 << 2; public static final int FLAG_DETECT_ACCESS_UNITS = 1 << 3; public static final int FLAG_IGNORE_SPLICE_INFO_STREAM = 1 << 4; + public static final int FLAG_OVERRIDE_CAPTION_DESCRIPTORS = 1 << 5; + + private static final int DESCRIPTOR_TAG_CAPTION_SERVICE = 0x86; @Flags private final int flags; private final List closedCaptionFormats; public DefaultTsPayloadReaderFactory() { - this(0); + this(0, Collections.emptyList()); } /** * @param flags A combination of {@code FLAG_*} values, which control the behavior of the created * readers. - */ - public DefaultTsPayloadReaderFactory(@Flags int flags) { - this(flags, Collections.singletonList(Format.createTextSampleFormat(null, - MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null))); - } - - /** - * @param flags A combination of {@code FLAG_*} values, which control the behavior of the created - * readers. - * @param closedCaptionFormats {@link Format}s to be exposed by elementary stream readers for - * streams with embedded closed captions. + * @param closedCaptionFormats {@link Format}s to be exposed by payload readers for streams with + * embedded closed captions when no caption service descriptors are provided. If + * {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, {@code closedCaptionFormats} overrides + * any descriptor information. If not set, and {@code closedCaptionFormats} is empty, a + * closed caption track with {@link Format#accessibilityChannel} {@link Format#NO_VALUE} will + * be exposed. */ public DefaultTsPayloadReaderFactory(@Flags int flags, List closedCaptionFormats) { this.flags = flags; + if (!isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS) && closedCaptionFormats.isEmpty()) { + closedCaptionFormats = Collections.singletonList(Format.createTextSampleFormat(null, + MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null)); + } this.closedCaptionFormats = closedCaptionFormats; } @@ -95,10 +100,10 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact return new PesReader(new H262Reader()); case TsExtractor.TS_STREAM_TYPE_H264: return isSet(FLAG_IGNORE_H264_STREAM) ? null - : new PesReader(new H264Reader(buildSeiReader(), isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES), - isSet(FLAG_DETECT_ACCESS_UNITS))); + : new PesReader(new H264Reader(buildSeiReader(esInfo), + isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES), isSet(FLAG_DETECT_ACCESS_UNITS))); case TsExtractor.TS_STREAM_TYPE_H265: - return new PesReader(new H265Reader(buildSeiReader())); + return new PesReader(new H265Reader(buildSeiReader(esInfo))); case TsExtractor.TS_STREAM_TYPE_SPLICE_INFO: return isSet(FLAG_IGNORE_SPLICE_INFO_STREAM) ? null : new SectionReader(new SpliceInfoSectionReader()); @@ -109,8 +114,52 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact } } - private SeiReader buildSeiReader() { - // TODO: Add descriptor parsing to detect channels automatically. + /** + * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link SeiReader} for + * {@link #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a + * {@link SeiReader} for the declared formats, or {@link #closedCaptionFormats} if the descriptor + * is not present. + * + * @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}. + * @return A {@link SeiReader} for closed caption tracks. + */ + private SeiReader buildSeiReader(EsInfo esInfo) { + if (isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS)) { + return new SeiReader(closedCaptionFormats); + } + ParsableByteArray scratchDescriptorData = new ParsableByteArray(esInfo.descriptorBytes); + List closedCaptionFormats = this.closedCaptionFormats; + while (scratchDescriptorData.bytesLeft() > 0) { + int descriptorTag = scratchDescriptorData.readUnsignedByte(); + int descriptorLength = scratchDescriptorData.readUnsignedByte(); + int nextDescriptorPosition = scratchDescriptorData.getPosition() + descriptorLength; + if (descriptorTag == DESCRIPTOR_TAG_CAPTION_SERVICE) { + // Note: see ATSC A/65 for detailed information about the caption service descriptor. + closedCaptionFormats = new ArrayList<>(); + int numberOfServices = scratchDescriptorData.readUnsignedByte() & 0x1F; + for (int i = 0; i < numberOfServices; i++) { + String language = scratchDescriptorData.readString(3); + int captionTypeByte = scratchDescriptorData.readUnsignedByte(); + boolean isDigital = (captionTypeByte & 0x80) != 0; + String mimeType; + int accessibilityChannel; + if (isDigital) { + mimeType = MimeTypes.APPLICATION_CEA708; + accessibilityChannel = captionTypeByte & 0x3F; + } else { + mimeType = MimeTypes.APPLICATION_CEA608; + accessibilityChannel = 1; + } + closedCaptionFormats.add(Format.createTextSampleFormat(null, mimeType, null, + Format.NO_VALUE, 0, language, accessibilityChannel, null)); + // Skip easy_reader(1), wide_aspect_ratio(1), reserved(14). + scratchDescriptorData.skipBytes(2); + } + } else { + // Unknown descriptor. Ignore. + } + scratchDescriptorData.setPosition(nextDescriptorPosition); + } return new SeiReader(closedCaptionFormats); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 357a32f086..cc3b1087e4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -356,6 +356,10 @@ import java.util.concurrent.atomic.AtomicInteger; // This flag ensures the change of pid between streams does not affect the sample queues. @DefaultTsPayloadReaderFactory.Flags int esReaderFactoryFlags = DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM; + if (!muxedCaptionFormats.isEmpty()) { + // The playlist declares closed caption renditions, we should ignore descriptors. + esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS; + } String codecs = trackFormat.codecs; if (!TextUtils.isEmpty(codecs)) { // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really From 3bb08e58f68ee1cc5a288b601c5fec3068cd83da Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 23 Feb 2017 14:27:23 +0000 Subject: [PATCH 071/140] Cleanup of CENC support --- .../java/com/google/android/exoplayer2/C.java | 7 +++--- .../drm/DefaultDrmSessionManager.java | 25 +++++-------------- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/C.java b/library/src/main/java/com/google/android/exoplayer2/C.java index 961550a174..a157fd85a9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/src/main/java/com/google/android/exoplayer2/C.java @@ -444,14 +444,15 @@ public final class C { public static final UUID UUID_NIL = new UUID(0L, 0L); /** - * UUID for the PSSH box and MPEG-DASH Content Protection. - * W3C. + * UUID for the + * CENC DRM + * scheme. */ public static final UUID CENC_UUID = new UUID(0x1077EFECC0B24D02L, 0xACE33C1E52E2FB4BL); /** * UUID for the Widevine DRM scheme. - *

    + *

    * Widevine is supported on Android devices running Android 4.3 (API Level 18) and up. */ public static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL); diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 68c88c4f10..bc57b7e810 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -103,12 +103,8 @@ public class DefaultDrmSessionManager implements DrmSe /** Releases an existing offline license. */ public static final int MODE_RELEASE = 3; - /** - * The format to use when ClearKey encryption. - */ - private static final String CENC_INIT_DATA_FORMAT = "cenc"; - private static final String TAG = "OfflineDrmSessionMngr"; + private static final String CENC_SCHEME_MIME_TYPE = "cenc"; private static final int MSG_PROVISION = 0; private static final int MSG_KEYS = 1; @@ -345,20 +341,11 @@ public class DefaultDrmSessionManager implements DrmSe schemeInitData = psshData; } } - if (C.CENC_UUID.equals(uuid)) { - // If "video/mp4" and "audio/mp4" are not supported as CENC schema, change it to "cenc". - // Before 7.1.x in API 25, "video/mp4" and "audio/mp4" are not supported. - if (MimeTypes.VIDEO_MP4.equals(schemeMimeType) || MimeTypes.AUDIO_MP4.equals( - schemeMimeType)) { - if (Util.SDK_INT >= 26) { - // Nothing to do. - } else if (Util.SDK_INT == 25 && !MediaDrm.isCryptoSchemeSupported(uuid, - schemeMimeType)) { - schemeMimeType = CENC_INIT_DATA_FORMAT; - } else if (Util.SDK_INT <= 24) { - schemeMimeType = CENC_INIT_DATA_FORMAT; - } - } + if (Util.SDK_INT < 26 && C.CENC_UUID.equals(uuid) + && (MimeTypes.VIDEO_MP4.equals(schemeMimeType) + || MimeTypes.AUDIO_MP4.equals(schemeMimeType))) { + // Prior to API level 26 the CDM only accepted "cenc" as the scheme mime type. + schemeMimeType = CENC_SCHEME_MIME_TYPE; } } state = STATE_OPENING; From 5fe5076c8686a09608bbef8797e1c16fa8d80fd8 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 23 Feb 2017 14:51:58 +0000 Subject: [PATCH 072/140] Clarify naming for ClearKey DRM support --- .../src/main/java/com/google/android/exoplayer2/C.java | 8 ++++---- .../android/exoplayer2/drm/DefaultDrmSessionManager.java | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/C.java b/library/src/main/java/com/google/android/exoplayer2/C.java index a157fd85a9..4f3e462dec 100644 --- a/library/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/src/main/java/com/google/android/exoplayer2/C.java @@ -444,11 +444,11 @@ public final class C { public static final UUID UUID_NIL = new UUID(0L, 0L); /** - * UUID for the - * CENC DRM - * scheme. + * UUID for the ClearKey DRM scheme. + *

    + * ClearKey is supported on Android devices running Android 5.0 (API Level 21) and up. */ - public static final UUID CENC_UUID = new UUID(0x1077EFECC0B24D02L, 0xACE33C1E52E2FB4BL); + public static final UUID CLEARKEY_UUID = new UUID(0x1077EFECC0B24D02L, 0xACE33C1E52E2FB4BL); /** * UUID for the Widevine DRM scheme. diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index bc57b7e810..6fc149ba32 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -341,10 +341,10 @@ public class DefaultDrmSessionManager implements DrmSe schemeInitData = psshData; } } - if (Util.SDK_INT < 26 && C.CENC_UUID.equals(uuid) + if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid) && (MimeTypes.VIDEO_MP4.equals(schemeMimeType) || MimeTypes.AUDIO_MP4.equals(schemeMimeType))) { - // Prior to API level 26 the CDM only accepted "cenc" as the scheme mime type. + // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4. schemeMimeType = CENC_SCHEME_MIME_TYPE; } } From dc17163351f0a05ce180b0acec484eecf1fee5ff Mon Sep 17 00:00:00 2001 From: wasabeef Date: Fri, 24 Feb 2017 00:47:09 +0900 Subject: [PATCH 073/140] Clarify naming for ClearKey DRM support --- .../google/android/exoplayer2/demo/SampleChooserActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 7377aa416d..d6655b79b9 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -268,7 +268,7 @@ public class SampleChooserActivity extends Activity { case "playready": return C.PLAYREADY_UUID; case "cenc": - return C.CENC_UUID; + return C.CLEARKEY_UUID; default: try { return UUID.fromString(typeString); From 82d33cde689fdd5888e1dcd2af877c1328cf8c01 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 23 Feb 2017 06:54:20 -0800 Subject: [PATCH 074/140] Add support for draining audio output. At the end of playback, BufferProcessors need to be drained to process all remaining data, then the output needs to be written to the AudioTrack before stop() is called. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148339194 --- demo/src/main/assets/media.exolist.json | 12 ---- .../android/exoplayer2/audio/AudioTrack.java | 64 ++++++++++++++++--- .../exoplayer2/audio/BufferProcessor.java | 15 +++++ .../audio/MediaCodecAudioRenderer.java | 10 ++- .../audio/ResamplingBufferProcessor.java | 15 ++++- .../audio/SimpleDecoderAudioRenderer.java | 9 ++- .../mediacodec/MediaCodecRenderer.java | 24 +++---- 7 files changed, 111 insertions(+), 38 deletions(-) diff --git a/demo/src/main/assets/media.exolist.json b/demo/src/main/assets/media.exolist.json index dd88f206c1..6fba5bd65b 100644 --- a/demo/src/main/assets/media.exolist.json +++ b/demo/src/main/assets/media.exolist.json @@ -277,18 +277,6 @@ } ] }, - { - "name": "ClearKey DASH", - "samples": [ - { - "name": "Big Buck Bunny (CENC ClearKey)", - "uri": "http://html5.cablelabs.com:8100/cenc/ck/dash.mpd", - "extension": "mpd", - "drm_scheme": "cenc", - "drm_license_url": "https://wasabeef.jp/demos/cenc-ck-dash.json" - } - ] - }, { "name": "SmoothStreaming", "samples": [ diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index 38085dfc3a..2f343ec40e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -54,8 +54,8 @@ import java.util.ArrayList; * safe to call {@link #handleBuffer(ByteBuffer, long)} after {@link #reset()} without calling * {@link #configure(String, int, int, int, int)}. *

    - * Call {@link #handleEndOfStream()} to play out all data when no more input buffers will be - * provided via {@link #handleBuffer(ByteBuffer, long)} until the next {@link #reset}. Call + * Call {@link #playToEndOfStream()} repeatedly to play out all data when no more input buffers will + * be provided via {@link #handleBuffer(ByteBuffer, long)} until the next {@link #reset}. Call * {@link #release()} when the instance is no longer required. */ public final class AudioTrack { @@ -324,6 +324,8 @@ public final class AudioTrack { private ByteBuffer outputBuffer; private byte[] preV21OutputBuffer; private int preV21OutputBufferOffset; + private int drainingBufferProcessorIndex; + private boolean handledEndOfStream; private boolean playing; private int audioSessionId; @@ -366,6 +368,7 @@ public final class AudioTrack { startMediaTimeState = START_NOT_SET; streamType = C.STREAM_TYPE_DEFAULT; audioSessionId = C.AUDIO_SESSION_ID_UNSET; + drainingBufferProcessorIndex = C.INDEX_UNSET; this.bufferProcessors = new BufferProcessor[0]; outputBuffers = new ByteBuffer[0]; } @@ -762,7 +765,8 @@ public final class AudioTrack { int count = bufferProcessors.length; int index = count; while (index >= 0) { - ByteBuffer input = index > 0 ? outputBuffers[index - 1] : inputBuffer; + ByteBuffer input = index > 0 ? outputBuffers[index - 1] + : (inputBuffer != null ? inputBuffer : BufferProcessor.EMPTY_BUFFER); if (index == count) { writeBuffer(input, avSyncPresentationTimeUs); } else { @@ -851,14 +855,54 @@ public final class AudioTrack { } /** - * Ensures that the last data passed to {@link #handleBuffer(ByteBuffer, long)} is played in full. + * Plays out remaining audio. {@link #isEnded()} will return {@code true} when playback has ended. + * + * @throws WriteException If an error occurs draining data to the track. */ - public void handleEndOfStream() { - if (isInitialized()) { - // TODO: Drain buffer processors before stopping the AudioTrack. - audioTrackUtil.handleEndOfStream(getWrittenFrames()); - bytesUntilNextAvSync = 0; + public void playToEndOfStream() throws WriteException { + if (handledEndOfStream || !isInitialized()) { + return; } + + // Drain the buffer processors. + boolean bufferProcessorNeedsEndOfStream = false; + if (drainingBufferProcessorIndex == C.INDEX_UNSET) { + drainingBufferProcessorIndex = passthrough ? bufferProcessors.length : 0; + bufferProcessorNeedsEndOfStream = true; + } + while (drainingBufferProcessorIndex < bufferProcessors.length) { + BufferProcessor bufferProcessor = bufferProcessors[drainingBufferProcessorIndex]; + if (bufferProcessorNeedsEndOfStream) { + bufferProcessor.queueEndOfStream(); + } + processBuffers(C.TIME_UNSET); + if (!bufferProcessor.isEnded()) { + return; + } + bufferProcessorNeedsEndOfStream = true; + drainingBufferProcessorIndex++; + } + + // Finish writing any remaining output to the track. + if (outputBuffer != null) { + writeBuffer(outputBuffer, C.TIME_UNSET); + if (outputBuffer != null) { + return; + } + } + + // Drain the track. + audioTrackUtil.handleEndOfStream(getWrittenFrames()); + bytesUntilNextAvSync = 0; + handledEndOfStream = true; + } + + /** + * Returns whether all buffers passed to {@link #handleBuffer(ByteBuffer, long)} have been + * completely processed and played. + */ + public boolean isEnded() { + return !isInitialized() || (handledEndOfStream && !hasPendingData()); } /** @@ -1003,6 +1047,8 @@ public final class AudioTrack { bufferProcessor.flush(); outputBuffers[i] = bufferProcessor.getOutput(); } + handledEndOfStream = false; + drainingBufferProcessorIndex = C.INDEX_UNSET; avSyncHeader = null; bytesUntilNextAvSync = 0; startMediaTimeState = START_NOT_SET; diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java index 0a58785ef2..d75eeb356f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java @@ -84,6 +84,15 @@ public interface BufferProcessor { */ void queueInput(ByteBuffer buffer); + /** + * Queues an end of stream signal and begins draining any pending output from this processor. + * After this method has been called, {@link #queueInput(ByteBuffer)} may not be called until + * after the next call to {@link #flush()}. Calling {@link #getOutput()} will return any remaining + * output data. Multiple calls may be required to read all of the remaining output data. + * {@link #isEnded()} will return {@code true} once all remaining output data has been read. + */ + void queueEndOfStream(); + /** * Returns a buffer containing processed output data between its position and limit. The buffer * will always be a direct byte buffer with native byte order. Calling this method invalidates any @@ -93,6 +102,12 @@ public interface BufferProcessor { */ ByteBuffer getOutput(); + /** + * Returns whether this processor will return no more output from {@link #getOutput()} until it + * has been {@link #flush()}ed and more input has been queued. + */ + boolean isEnded(); + /** * Clears any state in preparation for receiving a new stream of buffers. */ diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index dc7cdf42c8..7ab9d9133a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -312,7 +312,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override public boolean isEnded() { - return super.isEnded() && !audioTrack.hasPendingData(); + return super.isEnded() && audioTrack.isEnded(); } @Override @@ -361,8 +361,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - protected void onOutputStreamEnded() { - audioTrack.handleEndOfStream(); + protected void renderToEndOfStream() throws ExoPlaybackException { + try { + audioTrack.playToEndOfStream(); + } catch (AudioTrack.WriteException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java index 14bd58c3d8..343baf32e3 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java @@ -30,6 +30,7 @@ import java.nio.ByteOrder; private int encoding; private ByteBuffer buffer; private ByteBuffer outputBuffer; + private boolean inputEnded; /** * Creates a new buffer processor that converts audio data to {@link C#ENCODING_PCM_16BIT}. @@ -145,15 +146,27 @@ import java.nio.ByteOrder; return outputBuffer; } + @Override + public void queueEndOfStream() { + inputEnded = true; + } + + @SuppressWarnings("ReferenceEquality") + @Override + public boolean isEnded() { + return inputEnded && outputBuffer == EMPTY_BUFFER; + } + @Override public void flush() { outputBuffer = EMPTY_BUFFER; + inputEnded = false; } @Override public void release() { + flush(); buffer = EMPTY_BUFFER; - outputBuffer = EMPTY_BUFFER; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 3ca8c37e21..5e93aa920c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -178,6 +178,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { if (outputStreamEnded) { + try { + audioTrack.playToEndOfStream(); + } catch (AudioTrack.WriteException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } return; } @@ -280,7 +285,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements outputBuffer.release(); outputBuffer = null; outputStreamEnded = true; - audioTrack.handleEndOfStream(); + audioTrack.playToEndOfStream(); } return false; } @@ -388,7 +393,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override public boolean isEnded() { - return outputStreamEnded && !audioTrack.hasPendingData(); + return outputStreamEnded && audioTrack.isEnded(); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 9baf974b37..cf8d766c0c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -480,6 +480,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { if (outputStreamEnded) { + renderToEndOfStream(); return; } if (format == null) { @@ -787,16 +788,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { // Do nothing. } - /** - * Called when the output stream ends, meaning that the last output buffer has been processed and - * the {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag has been propagated through the decoder. - *

    - * The default implementation is a no-op. - */ - protected void onOutputStreamEnded() { - // Do nothing. - } - /** * Called immediately before an input buffer is queued into the codec. *

    @@ -1010,6 +1001,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer { MediaCodec codec, ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, boolean shouldSkip) throws ExoPlaybackException; + /** + * Incrementally renders any remaining output. + *

    + * The default implementation is a no-op. + * + * @throws ExoPlaybackException Thrown if an error occurs rendering remaining output. + */ + protected void renderToEndOfStream() throws ExoPlaybackException { + // Do nothing. + } + /** * Processes an end of stream signal. * @@ -1022,7 +1024,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { maybeInitCodec(); } else { outputStreamEnded = true; - onOutputStreamEnded(); + renderToEndOfStream(); } } From 563a3972840885f26c96c3206df693534797ab2b Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 23 Feb 2017 08:02:24 -0800 Subject: [PATCH 075/140] Merge remainder of https://github.com/google/ExoPlayer/pull/2372 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148344124 --- demo/src/main/assets/media.exolist.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/demo/src/main/assets/media.exolist.json b/demo/src/main/assets/media.exolist.json index 6fba5bd65b..dd88f206c1 100644 --- a/demo/src/main/assets/media.exolist.json +++ b/demo/src/main/assets/media.exolist.json @@ -277,6 +277,18 @@ } ] }, + { + "name": "ClearKey DASH", + "samples": [ + { + "name": "Big Buck Bunny (CENC ClearKey)", + "uri": "http://html5.cablelabs.com:8100/cenc/ck/dash.mpd", + "extension": "mpd", + "drm_scheme": "cenc", + "drm_license_url": "https://wasabeef.jp/demos/cenc-ck-dash.json" + } + ] + }, { "name": "SmoothStreaming", "samples": [ From e3a57146d232eac6485ea3effd8f804339a6a7e2 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 23 Feb 2017 08:03:59 -0800 Subject: [PATCH 076/140] Fix BufferProcessor.queueEndOfStream javadoc ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148344328 --- .../android/exoplayer2/audio/BufferProcessor.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java index d75eeb356f..c31823fd3b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java @@ -85,11 +85,11 @@ public interface BufferProcessor { void queueInput(ByteBuffer buffer); /** - * Queues an end of stream signal and begins draining any pending output from this processor. - * After this method has been called, {@link #queueInput(ByteBuffer)} may not be called until - * after the next call to {@link #flush()}. Calling {@link #getOutput()} will return any remaining - * output data. Multiple calls may be required to read all of the remaining output data. - * {@link #isEnded()} will return {@code true} once all remaining output data has been read. + * Queues an end of stream signal. After this method has been called, + * {@link #queueInput(ByteBuffer)} may not be called until after the next call to + * {@link #flush()}. Calling {@link #getOutput()} will return any remaining output data. Multiple + * calls may be required to read all of the remaining output data. {@link #isEnded()} will return + * {@code true} once all remaining output data has been read. */ void queueEndOfStream(); From ef2541e654edf74d21fa30dcefab324e6901c9aa Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 23 Feb 2017 08:50:47 -0800 Subject: [PATCH 077/140] Fix negative start time values Issue:#2495 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148348663 --- .../source/hls/playlist/HlsMediaPlaylistParserTest.java | 2 ++ .../exoplayer2/source/hls/playlist/HlsPlaylistParser.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java index 4286a283c0..3d976353cc 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java @@ -36,6 +36,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { String playlistString = "#EXTM3U\n" + "#EXT-X-VERSION:3\n" + "#EXT-X-PLAYLIST-TYPE:VOD\n" + + "#EXT-X-START:TIME-OFFSET=-25" + "#EXT-X-TARGETDURATION:8\n" + "#EXT-X-MEDIA-SEQUENCE:2679\n" + "#EXT-X-DISCONTINUITY-SEQUENCE:4\n" @@ -73,6 +74,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; assertEquals(HlsMediaPlaylist.PLAYLIST_TYPE_VOD, mediaPlaylist.playlistType); + assertEquals(mediaPlaylist.durationUs - 25000000, mediaPlaylist.startOffsetUs); assertEquals(2679, mediaPlaylist.mediaSequence); assertEquals(3, mediaPlaylist.version); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 6c29535326..d24264cae6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -81,7 +81,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Thu, 23 Feb 2017 13:30:19 -0800 Subject: [PATCH 078/140] Correctly handle a SampleStream ending without providing a format I'm going to introduce an EmptySampleStream that will be used in some cases in conjunction as part of 608/EMSG support. This change avoids EmptySampleStream having to provide a dummy format. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148383831 --- .../ext/vp9/LibvpxVideoRenderer.java | 33 ++++++---- .../android/exoplayer2/ExoPlayerTest.java | 7 +- .../android/exoplayer2/BaseRenderer.java | 11 ++-- .../audio/SimpleDecoderAudioRenderer.java | 52 +++++++++------ .../extractor/DefaultTrackOutput.java | 65 +++++++++++-------- .../mediacodec/MediaCodecRenderer.java | 25 ++++--- .../exoplayer2/metadata/MetadataRenderer.java | 2 +- .../source/ClippingMediaPeriod.java | 8 +-- .../source/ExtractorMediaPeriod.java | 12 ++-- .../exoplayer2/source/SampleStream.java | 14 ++-- .../source/SingleSampleMediaPeriod.java | 11 ++-- .../source/chunk/ChunkSampleStream.java | 6 +- .../source/hls/HlsSampleStream.java | 4 +- .../source/hls/HlsSampleStreamWrapper.java | 7 +- .../android/exoplayer2/text/TextRenderer.java | 2 +- 15 files changed, 155 insertions(+), 104 deletions(-) diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index e4cc2ae3ce..d0417bc37e 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -65,6 +65,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { private final boolean playClearSamplesWithoutKeys; private final EventDispatcher eventDispatcher; private final FormatHolder formatHolder; + private final DecoderInputBuffer flagsOnlyBuffer; private final DrmSessionManager drmSessionManager; private DecoderCounters decoderCounters; @@ -149,6 +150,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { joiningDeadlineMs = -1; clearLastReportedVideoSize(); formatHolder = new FormatHolder(); + flagsOnlyBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); eventDispatcher = new EventDispatcher(eventHandler, eventListener); outputMode = VpxDecoder.OUTPUT_MODE_NONE; } @@ -165,10 +167,22 @@ public final class LibvpxVideoRenderer extends BaseRenderer { return; } - // Try and read a format if we don't have one already. - if (format == null && !readFormat()) { - // We can't make progress without one. - return; + if (format == null) { + // We don't have a format yet, so try and read one. + flagsOnlyBuffer.clear(); + int result = readSource(formatHolder, flagsOnlyBuffer, true); + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder.format); + } else if (result == C.RESULT_BUFFER_READ) { + // End of stream read having not read a format. + Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); + inputStreamEnded = true; + outputStreamEnded = true; + return; + } else { + // We still don't have a format and can't make progress without one. + return; + } } if (isRendererAvailable()) { @@ -327,7 +341,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { // We've already read an encrypted sample into buffer, and are waiting for keys. result = C.RESULT_BUFFER_READ; } else { - result = readSource(formatHolder, inputBuffer); + result = readSource(formatHolder, inputBuffer, false); } if (result == C.RESULT_NOTHING_READ) { @@ -485,15 +499,6 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } - private boolean readFormat() throws ExoPlaybackException { - int result = readSource(formatHolder, null); - if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); - return true; - } - return false; - } - private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { Format oldFormat = format; format = newFormat; diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 1197139b01..2ad1159c3e 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -513,8 +513,9 @@ public final class ExoPlayerTest extends TestCase { } @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { - if (buffer == null || !readFormat) { + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { + if (formatRequired || !readFormat) { formatHolder.format = format; readFormat = true; return C.RESULT_FORMAT_READ; @@ -571,7 +572,7 @@ public final class ExoPlayerTest extends TestCase { FormatHolder formatHolder = new FormatHolder(); DecoderInputBuffer buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); - int result = readSource(formatHolder, buffer); + int result = readSource(formatHolder, buffer, false); if (result == C.RESULT_FORMAT_READ) { formatReadCount++; assertEquals(expectedFormat, formatHolder.format); diff --git a/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 9973a50cff..7266e2cd30 100644 --- a/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -262,13 +262,16 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the * end of the stream. If the end of the stream has been reached, the - * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. May be null if the - * caller requires that the format of the stream be read even if it's not changing. + * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. + * @param formatRequired Whether the caller requires that the format of the stream be read even if + * it's not changing. A sample will never be read if set to true, however it is still possible + * for the end of stream or nothing to be read. * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or * {@link C#RESULT_BUFFER_READ}. */ - protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer) { - int result = stream.readData(formatHolder, buffer); + protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { + int result = stream.readData(formatHolder, buffer, formatRequired); if (result == C.RESULT_BUFFER_READ) { if (buffer.isEndOfStream()) { readEndOfStream = true; diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 5e93aa920c..e80c9bb70a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MediaClock; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.TraceUtil; @@ -67,12 +68,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements */ private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2; + private final DrmSessionManager drmSessionManager; private final boolean playClearSamplesWithoutKeys; - private final EventDispatcher eventDispatcher; private final AudioTrack audioTrack; - private final DrmSessionManager drmSessionManager; private final FormatHolder formatHolder; + private final DecoderInputBuffer flagsOnlyBuffer; private DecoderCounters decoderCounters; private Format inputFormat; @@ -142,11 +143,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, BufferProcessor... bufferProcessors) { super(C.TRACK_TYPE_AUDIO); + this.drmSessionManager = drmSessionManager; + this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; eventDispatcher = new EventDispatcher(eventHandler, eventListener); audioTrack = new AudioTrack(audioCapabilities, bufferProcessors, new AudioTrackListener()); - this.drmSessionManager = drmSessionManager; formatHolder = new FormatHolder(); - this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; + flagsOnlyBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); decoderReinitializationState = REINITIALIZATION_STATE_NONE; audioTrackNeedsConfigure = true; } @@ -187,9 +189,22 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } // Try and read a format if we don't have one already. - if (inputFormat == null && !readFormat()) { - // We can't make progress without one. - return; + if (inputFormat == null) { + // We don't have a format yet, so try and read one. + flagsOnlyBuffer.clear(); + int result = readSource(formatHolder, flagsOnlyBuffer, true); + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder.format); + } else if (result == C.RESULT_BUFFER_READ) { + // End of stream read having not read a format. + Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); + inputStreamEnded = true; + processEndOfStream(); + return; + } else { + // We still don't have a format and can't make progress without one. + return; + } } // If we don't have a decoder yet, we need to instantiate one. @@ -284,8 +299,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } else { outputBuffer.release(); outputBuffer = null; - outputStreamEnded = true; - audioTrack.playToEndOfStream(); + processEndOfStream(); } return false; } @@ -334,7 +348,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements // We've already read an encrypted sample into buffer, and are waiting for keys. result = C.RESULT_BUFFER_READ; } else { - result = readSource(formatHolder, inputBuffer); + result = readSource(formatHolder, inputBuffer, false); } if (result == C.RESULT_NOTHING_READ) { @@ -375,6 +389,15 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements && (bufferEncrypted || !playClearSamplesWithoutKeys); } + private void processEndOfStream() throws ExoPlaybackException { + outputStreamEnded = true; + try { + audioTrack.playToEndOfStream(); + } catch (AudioTrack.WriteException e) { + throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); + } + } + private void flushDecoder() throws ExoPlaybackException { waitingForKeys = false; if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) { @@ -523,15 +546,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements decoderReceivedBuffers = false; } - private boolean readFormat() throws ExoPlaybackException { - int result = readSource(formatHolder, null); - if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); - return true; - } - return false; - } - private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { Format oldFormat = inputFormat; inputFormat = newFormat; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java b/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java index 460e8d33a8..8aff8858a1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java @@ -267,40 +267,42 @@ public final class DefaultTrackOutput implements TrackOutput { * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the * end of the stream. If the end of the stream has been reached, the - * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. May be null if the - * caller requires that the format of the stream be read even if it's not changing. + * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. + * @param formatRequired Whether the caller requires that the format of the stream be read even if + * it's not changing. A sample will never be read if set to true, however it is still possible + * for the end of stream or nothing to be read. * @param loadingFinished True if an empty queue should be considered the end of the stream. * @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will * be set if the buffer's timestamp is less than this value. * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or * {@link C#RESULT_BUFFER_READ}. */ - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean loadingFinished, - long decodeOnlyUntilUs) { - switch (infoQueue.readData(formatHolder, buffer, downstreamFormat, extrasHolder)) { - case C.RESULT_NOTHING_READ: - if (loadingFinished) { - buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); - return C.RESULT_BUFFER_READ; - } - return C.RESULT_NOTHING_READ; + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, + boolean loadingFinished, long decodeOnlyUntilUs) { + int result = infoQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, + downstreamFormat, extrasHolder); + switch (result) { case C.RESULT_FORMAT_READ: downstreamFormat = formatHolder.format; return C.RESULT_FORMAT_READ; case C.RESULT_BUFFER_READ: - if (buffer.timeUs < decodeOnlyUntilUs) { - buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); + if (!buffer.isEndOfStream()) { + if (buffer.timeUs < decodeOnlyUntilUs) { + buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); + } + // Read encryption data if the sample is encrypted. + if (buffer.isEncrypted()) { + readEncryptionData(buffer, extrasHolder); + } + // Write the sample data into the holder. + buffer.ensureSpaceForWrite(extrasHolder.size); + readData(extrasHolder.offset, buffer.data, extrasHolder.size); + // Advance the read head. + dropDownstreamTo(extrasHolder.nextOffset); } - // Read encryption data if the sample is encrypted. - if (buffer.isEncrypted()) { - readEncryptionData(buffer, extrasHolder); - } - // Write the sample data into the holder. - buffer.ensureSpaceForWrite(extrasHolder.size); - readData(extrasHolder.offset, buffer.data, extrasHolder.size); - // Advance the read head. - dropDownstreamTo(extrasHolder.nextOffset); return C.RESULT_BUFFER_READ; + case C.RESULT_NOTHING_READ: + return C.RESULT_NOTHING_READ; default: throw new IllegalStateException(); } @@ -760,23 +762,34 @@ public final class DefaultTrackOutput implements TrackOutput { * and the absolute position of the first byte that may still be required after the current * sample has been read. May be null if the caller requires that the format of the stream be * read even if it's not changing. + * @param formatRequired Whether the caller requires that the format of the stream be read even + * if it's not changing. A sample will never be read if set to true, however it is still + * possible for the end of stream or nothing to be read. + * @param loadingFinished True if an empty queue should be considered the end of the stream. * @param downstreamFormat The current downstream {@link Format}. If the format of the next * sample is different to the current downstream format then a format will be read. * @param extrasHolder The holder into which extra sample information should be written. * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} * or {@link C#RESULT_BUFFER_READ}. */ + @SuppressWarnings("ReferenceEquality") public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, - Format downstreamFormat, BufferExtrasHolder extrasHolder) { + boolean formatRequired, boolean loadingFinished, Format downstreamFormat, + BufferExtrasHolder extrasHolder) { if (queueSize == 0) { - if (upstreamFormat != null && (buffer == null || upstreamFormat != downstreamFormat)) { + if (loadingFinished) { + buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } else if (upstreamFormat != null + && (formatRequired || upstreamFormat != downstreamFormat)) { formatHolder.format = upstreamFormat; return C.RESULT_FORMAT_READ; + } else { + return C.RESULT_NOTHING_READ; } - return C.RESULT_NOTHING_READ; } - if (buffer == null || formats[relativeReadIndex] != downstreamFormat) { + if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { formatHolder.format = formats[relativeReadIndex]; return C.RESULT_FORMAT_READ; } diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index cf8d766c0c..3fbbfac652 100644 --- a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -484,7 +484,21 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return; } if (format == null) { - readFormat(); + // We don't have a format yet, so try and read one. + buffer.clear(); + int result = readSource(formatHolder, buffer, true); + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder.format); + } else if (result == C.RESULT_BUFFER_READ) { + // End of stream read having not read a format. + Assertions.checkState(buffer.isEndOfStream()); + inputStreamEnded = true; + processEndOfStream(); + return; + } else { + // We still don't have a format and can't make progress without one. + return; + } } maybeInitCodec(); if (codec != null) { @@ -498,13 +512,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { decoderCounters.ensureUpdated(); } - private void readFormat() throws ExoPlaybackException { - int result = readSource(formatHolder, null); - if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); - } - } - protected void flushCodec() throws ExoPlaybackException { codecHotswapDeadlineMs = C.TIME_UNSET; inputIndex = C.INDEX_UNSET; @@ -594,7 +601,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING; } adaptiveReconfigurationBytes = buffer.data.position(); - result = readSource(formatHolder, buffer); + result = readSource(formatHolder, buffer, false); } if (result == C.RESULT_NOTHING_READ) { diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index 550a13771f..6c2ef319fd 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -109,7 +109,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { if (!inputStreamEnded && pendingMetadata == null) { buffer.clear(); - int result = readSource(formatHolder, buffer); + int result = readSource(formatHolder, buffer, false); if (result == C.RESULT_BUFFER_READ) { if (buffer.isEndOfStream()) { inputStreamEnded = true; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index b18eabf493..51663a21c6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -231,18 +231,16 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean requireFormat) { if (pendingDiscontinuity) { return C.RESULT_NOTHING_READ; } - if (buffer == null) { - return stream.readData(formatHolder, null); - } if (sentEos) { buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); return C.RESULT_BUFFER_READ; } - int result = stream.readData(formatHolder, buffer); + int result = stream.readData(formatHolder, buffer, requireFormat); // TODO: Clear gapless playback metadata if a format was read (if applicable). if (endUs != C.TIME_END_OF_SOURCE && ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs) || (result == C.RESULT_NOTHING_READ diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index dc189058a6..97e9ddd7e7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -325,13 +325,14 @@ import java.io.IOException; loader.maybeThrowError(); } - /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer) { + /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { if (notifyReset || isPendingReset()) { return C.RESULT_NOTHING_READ; } - return sampleQueues.valueAt(track).readData(formatHolder, buffer, loadingFinished, - lastSeekPositionUs); + return sampleQueues.valueAt(track).readData(formatHolder, buffer, formatRequired, + loadingFinished, lastSeekPositionUs); } // Loader.Callback implementation. @@ -552,8 +553,9 @@ import java.io.IOException; } @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { - return ExtractorMediaPeriod.this.readData(track, formatHolder, buffer); + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { + return ExtractorMediaPeriod.this.readData(track, formatHolder, buffer, formatRequired); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SampleStream.java b/library/src/main/java/com/google/android/exoplayer2/source/SampleStream.java index 5ee70cd2ed..90153d1790 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/SampleStream.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/SampleStream.java @@ -45,20 +45,24 @@ public interface SampleStream { /** * Attempts to read from the stream. *

    - * If no data is available then {@link C#RESULT_NOTHING_READ} is returned. If the format of the - * media is changing or if {@code buffer == null} then {@code formatHolder} is populated and + * If the stream has ended then {@link C#BUFFER_FLAG_END_OF_STREAM} flag is set on {@code buffer} + * and {@link C#RESULT_BUFFER_READ} is returned. Else if no data is available then + * {@link C#RESULT_NOTHING_READ} is returned. Else if the format of the media is changing or if + * {@code formatRequired} is set then {@code formatHolder} is populated and * {@link C#RESULT_FORMAT_READ} is returned. Else {@code buffer} is populated and * {@link C#RESULT_BUFFER_READ} is returned. * * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the * end of the stream. If the end of the stream has been reached, the - * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. May be null if the - * caller requires that the format of the stream be read even if it's not changing. + * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. + * @param formatRequired Whether the caller requires that the format of the stream be read even if + * it's not changing. A sample will never be read if set to true, however it is still possible + * for the end of stream or nothing to be read. * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or * {@link C#RESULT_BUFFER_READ}. */ - int readData(FormatHolder formatHolder, DecoderInputBuffer buffer); + int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired); /** * Attempts to skip to the keyframe before the specified time. diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index c78bb5371b..fd2ebffe8e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -205,14 +205,15 @@ import java.util.Arrays; } @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { - if (buffer == null || streamState == STREAM_STATE_SEND_FORMAT) { + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean requireFormat) { + if (streamState == STREAM_STATE_END_OF_STREAM) { + buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } else if (requireFormat || streamState == STREAM_STATE_SEND_FORMAT) { formatHolder.format = format; streamState = STREAM_STATE_SEND_SAMPLE; return C.RESULT_FORMAT_READ; - } else if (streamState == STREAM_STATE_END_OF_STREAM) { - buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); - return C.RESULT_BUFFER_READ; } Assertions.checkState(streamState == STREAM_STATE_SEND_SAMPLE); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 3955d64034..7149ce3f99 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -169,7 +169,8 @@ public class ChunkSampleStream implements SampleStream, S } @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { if (isPendingReset()) { return C.RESULT_NOTHING_READ; } @@ -187,7 +188,8 @@ public class ChunkSampleStream implements SampleStream, S currentChunk.startTimeUs); } downstreamTrackFormat = trackFormat; - return sampleQueue.readData(formatHolder, buffer, loadingFinished, lastSeekPositionUs); + return sampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, + lastSeekPositionUs); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java index 04fe8a093c..d8eb7e1ae8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java @@ -45,8 +45,8 @@ import java.io.IOException; } @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { - return sampleStreamWrapper.readData(group, formatHolder, buffer); + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) { + return sampleStreamWrapper.readData(group, formatHolder, buffer, requireFormat); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 0e3ee6fa9c..8bd966f177 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -290,7 +290,8 @@ import java.util.LinkedList; chunkSource.maybeThrowError(); } - /* package */ int readData(int group, FormatHolder formatHolder, DecoderInputBuffer buffer) { + /* package */ int readData(int group, FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean requireFormat) { if (isPendingReset()) { return C.RESULT_NOTHING_READ; } @@ -307,8 +308,8 @@ import java.util.LinkedList; } downstreamTrackFormat = trackFormat; - return sampleQueues.valueAt(group).readData(formatHolder, buffer, loadingFinished, - lastSeekPositionUs); + return sampleQueues.valueAt(group).readData(formatHolder, buffer, requireFormat, + loadingFinished, lastSeekPositionUs); } /* package */ void skipToKeyframeBefore(int group, long timeUs) { diff --git a/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index 649575865e..a7e05a010a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -189,7 +189,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { } } // Try and read the next subtitle from the source. - int result = readSource(formatHolder, nextInputBuffer); + int result = readSource(formatHolder, nextInputBuffer, false); if (result == C.RESULT_BUFFER_READ) { if (nextInputBuffer.isEndOfStream()) { inputStreamEnded = true; From 88fc337db0dfcb606f414a35739b011cbce85fb1 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 23 Feb 2017 14:20:34 -0800 Subject: [PATCH 079/140] Expose empty CEA-608 and EMSG tracks for DASH This change exposes declared CEA-608 and EMSG tracks. The tracks currently provide no samples. Issue: #2362 Issue: #2176 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148390849 --- .../exoplayer2/source/EmptySampleStream.java | 50 ++++++++++++ .../source/dash/DashMediaPeriod.java | 81 ++++++++++++------- 2 files changed, 103 insertions(+), 28 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java b/library/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java new file mode 100644 index 0000000000..eb94351f61 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2017 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.source; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import java.io.IOException; + +/** + * An empty {@link SampleStream}. + */ +public final class EmptySampleStream implements SampleStream { + + @Override + public boolean isReady() { + return true; + } + + @Override + public void maybeThrowError() throws IOException { + // Do nothing. + } + + @Override + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { + buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } + + @Override + public void skipToKeyframeBefore(long timeUs) { + // Do nothing. + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index cab956ccfe..e89deb53ab 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoader; +import com.google.android.exoplayer2.source.EmptySampleStream; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; @@ -27,12 +28,12 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.chunk.ChunkSampleStream; import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; -import com.google.android.exoplayer2.source.dash.manifest.Period; import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.source.dash.manifest.SchemeValuePair; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; +import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -56,16 +57,16 @@ import java.util.List; private ChunkSampleStream[] sampleStreams; private CompositeSequenceableLoader sequenceableLoader; private DashManifest manifest; - private int index; - private Period period; + private int periodIndex; + private List adaptationSets; - public DashMediaPeriod(int id, DashManifest manifest, int index, + public DashMediaPeriod(int id, DashManifest manifest, int periodIndex, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, EventDispatcher eventDispatcher, long elapsedRealtimeOffset, LoaderErrorThrower manifestLoaderErrorThrower, Allocator allocator) { this.id = id; this.manifest = manifest; - this.index = index; + this.periodIndex = periodIndex; this.chunkSourceFactory = chunkSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.eventDispatcher = eventDispatcher; @@ -74,17 +75,17 @@ import java.util.List; this.allocator = allocator; sampleStreams = newSampleStreamArray(0); sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); - period = manifest.getPeriod(index); - trackGroups = buildTrackGroups(period); + adaptationSets = manifest.getPeriod(periodIndex).adaptationSets; + trackGroups = buildTrackGroups(adaptationSets); } - public void updateManifest(DashManifest manifest, int index) { + public void updateManifest(DashManifest manifest, int periodIndex) { this.manifest = manifest; - this.index = index; - period = manifest.getPeriod(index); + this.periodIndex = periodIndex; + adaptationSets = manifest.getPeriod(periodIndex).adaptationSets; if (sampleStreams != null) { for (ChunkSampleStream sampleStream : sampleStreams) { - sampleStream.getChunkSource().updateManifest(manifest, index); + sampleStream.getChunkSource().updateManifest(manifest, periodIndex); } callback.onContinueLoadingRequested(this); } @@ -117,7 +118,7 @@ import java.util.List; SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { ArrayList> sampleStreamsList = new ArrayList<>(); for (int i = 0; i < selections.length; i++) { - if (streams[i] != null) { + if (streams[i] instanceof ChunkSampleStream) { @SuppressWarnings("unchecked") ChunkSampleStream stream = (ChunkSampleStream) streams[i]; if (selections[i] == null || !mayRetainStreamFlags[i]) { @@ -126,11 +127,21 @@ import java.util.List; } else { sampleStreamsList.add(stream); } + } else if (streams[i] instanceof EmptySampleStream && selections[i] == null) { + // TODO: Release streams for cea-608 and emsg tracks. + streams[i] = null; } if (streams[i] == null && selections[i] != null) { - ChunkSampleStream stream = buildSampleStream(selections[i], positionUs); - sampleStreamsList.add(stream); - streams[i] = stream; + int adaptationSetIndex = trackGroups.indexOf(selections[i].getTrackGroup()); + if (adaptationSetIndex < adaptationSets.size()) { + ChunkSampleStream stream = buildSampleStream(adaptationSetIndex, + selections[i], positionUs); + sampleStreamsList.add(stream); + streams[i] = stream; + } else { + // TODO: Output streams for cea-608 and emsg tracks. + streams[i] = new EmptySampleStream(); + } streamResetFlags[i] = true; } } @@ -184,35 +195,50 @@ import java.util.List; // Internal methods. - private static TrackGroupArray buildTrackGroups(Period period) { - TrackGroup[] trackGroupArray = new TrackGroup[period.adaptationSets.size()]; - for (int i = 0; i < period.adaptationSets.size(); i++) { - AdaptationSet adaptationSet = period.adaptationSets.get(i); + private static TrackGroupArray buildTrackGroups(List adaptationSets) { + int adaptationSetCount = adaptationSets.size(); + int eventMessageTrackCount = getEventMessageTrackCount(adaptationSets); + int cea608TrackCount = getCea608TrackCount(adaptationSets); + TrackGroup[] trackGroupArray = new TrackGroup[adaptationSetCount + eventMessageTrackCount + + cea608TrackCount]; + int eventMessageTrackIndex = 0; + int cea608TrackIndex = 0; + for (int i = 0; i < adaptationSetCount; i++) { + AdaptationSet adaptationSet = adaptationSets.get(i); List representations = adaptationSet.representations; Format[] formats = new Format[representations.size()]; for (int j = 0; j < formats.length; j++) { formats[j] = representations.get(j).format; } trackGroupArray[i] = new TrackGroup(formats); + if (hasEventMessageTrack(adaptationSet)) { + Format format = Format.createSampleFormat(adaptationSet.id + ":emsg", + MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null); + trackGroupArray[adaptationSetCount + eventMessageTrackIndex++] = new TrackGroup(format); + } + if (hasCea608Track(adaptationSet)) { + Format format = Format.createTextSampleFormat(adaptationSet.id + ":cea608", + MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null); + trackGroupArray[adaptationSetCount + eventMessageTrackCount + cea608TrackIndex++] = + new TrackGroup(format); + } } return new TrackGroupArray(trackGroupArray); } - private ChunkSampleStream buildSampleStream(TrackSelection selection, - long positionUs) { - int adaptationSetIndex = trackGroups.indexOf(selection.getTrackGroup()); - AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex); + private ChunkSampleStream buildSampleStream(int adaptationSetIndex, + TrackSelection selection, long positionUs) { + AdaptationSet adaptationSet = adaptationSets.get(adaptationSetIndex); boolean enableEventMessageTrack = hasEventMessageTrack(adaptationSet); boolean enableCea608Track = hasCea608Track(adaptationSet); DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource( - manifestLoaderErrorThrower, manifest, index, adaptationSetIndex, selection, + manifestLoaderErrorThrower, manifest, periodIndex, adaptationSetIndex, selection, elapsedRealtimeOffset, enableEventMessageTrack, enableCea608Track); return new ChunkSampleStream<>(adaptationSet.type, chunkSource, this, allocator, positionUs, minLoadableRetryCount, eventDispatcher); } - private static int getEventMessageTrackCount(Period period) { - List adaptationSets = period.adaptationSets; + private static int getEventMessageTrackCount(List adaptationSets) { int inbandEventStreamTrackCount = 0; for (int i = 0; i < adaptationSets.size(); i++) { if (hasEventMessageTrack(adaptationSets.get(i))) { @@ -233,8 +259,7 @@ import java.util.List; return false; } - private static int getCea608TrackCount(Period period) { - List adaptationSets = period.adaptationSets; + private static int getCea608TrackCount(List adaptationSets) { int cea608TrackCount = 0; for (int i = 0; i < adaptationSets.size(); i++) { if (hasCea608Track(adaptationSets.get(i))) { From 84def0d0480f4413c28836e1b0af97ecb5d92706 Mon Sep 17 00:00:00 2001 From: anjalibh Date: Thu, 23 Feb 2017 15:59:00 -0800 Subject: [PATCH 080/140] Implement VP9 profile 2 - 10 bit BT2020 support with libvpx. This code truncates the 10 bits to 8. We'll later update this to upload half-float or 16 bit short textures. Pending: Convert BT2020 to DCI-P3 before render. I'll add the same code to V2 after initial review. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148403349 --- .../assets/roadtrip-vp92-10bit.webm | Bin 0 -> 121221 bytes .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 13 ++++ .../exoplayer2/ext/vp9/VpxLibrary.java | 10 +++ .../exoplayer2/ext/vp9/VpxOutputBuffer.java | 1 + .../exoplayer2/ext/vp9/VpxRenderer.java | 25 ++++++-- extensions/vp9/src/main/jni/vpx_jni.cc | 58 +++++++++++++++--- 6 files changed, 93 insertions(+), 14 deletions(-) create mode 100644 extensions/vp9/src/androidTest/assets/roadtrip-vp92-10bit.webm diff --git a/extensions/vp9/src/androidTest/assets/roadtrip-vp92-10bit.webm b/extensions/vp9/src/androidTest/assets/roadtrip-vp92-10bit.webm new file mode 100644 index 0000000000000000000000000000000000000000..b3bd1b9d7484c61b93fe3faef5d5efd5964cad8a GIT binary patch literal 121221 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

      ?ScAb5>k<$rmv$L&T%6ozwUi*jIDeta@a^?_ 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 0; + } + private static native String vpxGetVersion(); private static native String vpxGetBuildConfig(); public static native boolean vpxIsSecureDecodeSupported(); diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java index c76d0eda03..db3cf49b0c 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java @@ -26,6 +26,7 @@ import java.nio.ByteBuffer; public static final int COLORSPACE_UNKNOWN = 0; public static final int COLORSPACE_BT601 = 1; public static final int COLORSPACE_BT709 = 2; + public static final int COLORSPACE_BT2020 = 3; private final VpxDecoder owner; diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java index d108ae8b4f..837539593e 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java @@ -42,6 +42,12 @@ import javax.microedition.khronos.opengles.GL10; 1.793f, -0.533f, 0.0f, }; + private static final float[] kColorConversion2020 = { + 1.168f, 1.168f, 1.168f, + 0.0f, -0.188f, 2.148f, + 1.683f, -0.652f, 0.0f, + }; + private static final String VERTEX_SHADER = "varying vec2 interp_tc;\n" + "attribute vec4 in_pos;\n" @@ -59,12 +65,13 @@ import javax.microedition.khronos.opengles.GL10; + "uniform sampler2D v_tex;\n" + "uniform mat3 mColorConversion;\n" + "void main() {\n" - + " vec3 yuv;" + + " vec3 yuv;\n" + " yuv.x = texture2D(y_tex, interp_tc).r - 0.0625;\n" + " yuv.y = texture2D(u_tex, interp_tc).r - 0.5;\n" + " yuv.z = texture2D(v_tex, interp_tc).r - 0.5;\n" - + " gl_FragColor = vec4(mColorConversion * yuv, 1.0);" + + " gl_FragColor = vec4(mColorConversion * yuv, 1.0);\n" + "}\n"; + private static final FloatBuffer TEXTURE_VERTICES = nativeFloatBuffer( -1.0f, 1.0f, -1.0f, -1.0f, @@ -156,8 +163,18 @@ import javax.microedition.khronos.opengles.GL10; } VpxOutputBuffer outputBuffer = renderedOutputBuffer; // Set color matrix. Assume BT709 if the color space is unknown. - float[] colorConversion = outputBuffer.colorspace == VpxOutputBuffer.COLORSPACE_BT601 - ? kColorConversion601 : kColorConversion709; + float[] colorConversion = kColorConversion709; + switch (outputBuffer.colorspace) { + case VpxOutputBuffer.COLORSPACE_BT601: + colorConversion = kColorConversion601; + break; + case VpxOutputBuffer.COLORSPACE_BT2020: + colorConversion = kColorConversion2020; + break; + case VpxOutputBuffer.COLORSPACE_BT709: + default: + break; // Do nothing + } GLES20.glUniformMatrix3fv(colorMatrixLocation, 1, false, colorConversion, 0); for (int i = 0; i < 3; i++) { diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index 137ff9ac21..67fd250fdc 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -74,8 +74,11 @@ DECODER_FUNC(jlong, vpxInit) { vpx_codec_dec_cfg_t cfg = {0, 0, 0}; cfg.threads = android_getCpuCount(); errorCode = 0; - if (vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo, &cfg, 0)) { - LOGE("ERROR: Fail to initialize libvpx decoder."); + vpx_codec_err_t err = vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo, + &cfg, 0); + if (err) { + LOGE("ERROR: Failed to initialize libvpx decoder, error = %d.", err); + errorCode = err; return 0; } @@ -160,6 +163,7 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { const int kColorspaceUnknown = 0; const int kColorspaceBT601 = 1; const int kColorspaceBT709 = 2; + const int kColorspaceBT2020 = 3; int colorspace = kColorspaceUnknown; switch (img->cs) { @@ -169,6 +173,9 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { case VPX_CS_BT_709: colorspace = kColorspaceBT709; break; + case VPX_CS_BT_2020: + colorspace = kColorspaceBT2020; + break; default: break; } @@ -186,14 +193,45 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { jbyte* const data = reinterpret_cast(env->GetDirectBufferAddress(dataObject)); - // TODO: This copy can be eliminated by using external frame buffers. NOLINT - // This is insignificant for smaller videos but takes ~1.5ms for 1080p - // clips. So this should eventually be gotten rid of. - const uint64_t y_length = img->stride[VPX_PLANE_Y] * img->d_h; - const uint64_t uv_length = img->stride[VPX_PLANE_U] * ((img->d_h + 1) / 2); - memcpy(data, img->planes[VPX_PLANE_Y], y_length); - memcpy(data + y_length, img->planes[VPX_PLANE_U], uv_length); - memcpy(data + y_length + uv_length, img->planes[VPX_PLANE_V], uv_length); + const int32_t uvHeight = (img->d_h + 1) / 2; + const uint64_t yLength = img->stride[VPX_PLANE_Y] * img->d_h; + const uint64_t uvLength = img->stride[VPX_PLANE_U] * uvHeight; + if (img->fmt == VPX_IMG_FMT_I42016) { // HBD planar 420. + // Note: The stride for BT2020 is twice of what we use so this is wasting + // memory. The long term goal however is to upload half-float/short so + // it's not important to optimize the stride at this time. + // Y + for (int y = 0; y < img->d_h; y++) { + const uint16_t* srcBase = reinterpret_cast( + img->planes[VPX_PLANE_Y] + img->stride[VPX_PLANE_Y] * y); + int8_t* destBase = data + img->stride[VPX_PLANE_Y] * y; + for (int x = 0; x < img->d_w; x++) { + *destBase++ = *srcBase++ / 4; + } + } + // UV + const int32_t uvWidth = (img->d_w + 1) / 2; + for (int y = 0; y < uvHeight; y++) { + const uint16_t* srcUBase = reinterpret_cast( + img->planes[VPX_PLANE_U] + img->stride[VPX_PLANE_U] * y); + const uint16_t* srcVBase = reinterpret_cast( + img->planes[VPX_PLANE_V] + img->stride[VPX_PLANE_V] * y); + int8_t* destUBase = data + yLength + img->stride[VPX_PLANE_U] * y; + int8_t* destVBase = data + yLength + uvLength + + img->stride[VPX_PLANE_V] * y; + for (int x = 0; x < uvWidth; x++) { + *destUBase++ = *srcUBase++ / 4; + *destVBase++ = *srcVBase++ / 4; + } + } + } else { + // TODO: This copy can be eliminated by using external frame buffers. This + // is insignificant for smaller videos but takes ~1.5ms for 1080p clips. + // So this should eventually be gotten rid of. + memcpy(data, img->planes[VPX_PLANE_Y], yLength); + memcpy(data + yLength, img->planes[VPX_PLANE_U], uvLength); + memcpy(data + yLength + uvLength, img->planes[VPX_PLANE_V], uvLength); + } } return 0; } From 5985f28e1baf121572d51ec954aa865e088cc0b6 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 24 Feb 2017 02:18:22 -0800 Subject: [PATCH 081/140] Add GVR extension and GvrBufferProcessor. A GvrBufferProcessor can be passed to the player by overriding SimpleExoPlayer's buildBufferProcessors method. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148442114 --- extensions/gvr/README.md | 19 +++++++++++++++++++ extensions/gvr/build.gradle | 30 ++++++++++++++++++++++++++++++ settings.gradle | 13 ++++++------- 3 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 extensions/gvr/README.md create mode 100644 extensions/gvr/build.gradle diff --git a/extensions/gvr/README.md b/extensions/gvr/README.md new file mode 100644 index 0000000000..b6be1705a2 --- /dev/null +++ b/extensions/gvr/README.md @@ -0,0 +1,19 @@ +# ExoPlayer GVR Extension # + +## Description ## + +The GVR extension wraps the [Google VR SDK for Android][]. It provides a +GvrBufferProcessor, which uses [GvrAudioSurround][] to provide binaural +rendering of surround sound and ambisonic soundfields. + +## Instructions ## + +If using SimpleExoPlayer, override SimpleExoPlayer.buildBufferProcessors to +return a GvrBufferProcessor. + +If constructing renderers directly, pass a GvrBufferProcessor to +MediaCodecAudioRenderer's constructor. + +[Google VR SDK for Android]: https://developers.google.com/vr/android/ +[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround + diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle new file mode 100644 index 0000000000..5ee9f45509 --- /dev/null +++ b/extensions/gvr/build.gradle @@ -0,0 +1,30 @@ +// Copyright (C) 2017 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. +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.ext.compileSdkVersion + buildToolsVersion project.ext.buildToolsVersion + + defaultConfig { + // Required by com.google.vr:sdk-audio. + minSdkVersion 19 + targetSdkVersion project.ext.targetSdkVersion + } +} + +dependencies { + compile project(':library') + compile 'com.google.vr:sdk-audio:1.20.0' +} diff --git a/settings.gradle b/settings.gradle index 8500dc6af7..d2d9cc87ed 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,20 +15,19 @@ include ':library' include ':testutils' include ':demo' include ':playbacktests' +include ':extension-ffmpeg' +include ':extension-flac' +include ':extension-okhttp' include ':extension-opus' include ':extension-vp9' -include ':extension-okhttp' -include ':extension-flac' -include ':extension-ffmpeg' // Uncomment the following line to use the Cronet Extension. // include ':extension-cronet' - +project(':extension-ffmpeg').projectDir = new File(settingsDir, 'extensions/ffmpeg') +project(':extension-flac').projectDir = new File(settingsDir, 'extensions/flac') +project(':extension-okhttp').projectDir = new File(settingsDir, 'extensions/okhttp') project(':extension-opus').projectDir = new File(settingsDir, 'extensions/opus') project(':extension-vp9').projectDir = new File(settingsDir, 'extensions/vp9') -project(':extension-okhttp').projectDir = new File(settingsDir, 'extensions/okhttp') -project(':extension-flac').projectDir = new File(settingsDir, 'extensions/flac') -project(':extension-ffmpeg').projectDir = new File(settingsDir, 'extensions/ffmpeg') // Uncomment the following line to use the Cronet Extension. // See extensions/cronet/README.md for details. // project(':extension-cronet').projectDir = new File(settingsDir, 'extensions/cronet') From 129334d2a0b292d9e6fcca3471453ce8a2d84c6d Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 24 Feb 2017 03:07:48 -0800 Subject: [PATCH 082/140] Clean up method ordering in ResamplingBufferProcessor. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148444806 --- .../exoplayer2/audio/ResamplingBufferProcessor.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java index 343baf32e3..9d06c4718f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java @@ -139,6 +139,11 @@ import java.nio.ByteOrder; outputBuffer = buffer; } + @Override + public void queueEndOfStream() { + inputEnded = true; + } + @Override public ByteBuffer getOutput() { ByteBuffer outputBuffer = this.outputBuffer; @@ -146,11 +151,6 @@ import java.nio.ByteOrder; return outputBuffer; } - @Override - public void queueEndOfStream() { - inputEnded = true; - } - @SuppressWarnings("ReferenceEquality") @Override public boolean isEnded() { From ad857852e52972d6686b7020922f338374b07a63 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 24 Feb 2017 11:19:42 -0800 Subject: [PATCH 083/140] Discard subtitles with invalid positions textWidth can be negative if textLeft extends parentRight (i.e. the subtitle is positioned entirely off the screen to the RHS). We should just discard and log a warning in this case. Issue: #2497 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148481310 --- .../com/google/android/exoplayer2/ui/SubtitlePainter.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index 5ca97403f1..d4f09b1721 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -266,6 +266,12 @@ import com.google.android.exoplayer2.util.Util; textRight = textLeft + textWidth; } + textWidth = textRight - textLeft; + if (textWidth <= 0) { + Log.w(TAG, "Skipped drawing subtitle cue (invalid horizontal positioning)"); + return; + } + int textTop; if (cueLine != Cue.DIMEN_UNSET) { int anchorPosition; @@ -292,8 +298,6 @@ import com.google.android.exoplayer2.util.Util; textTop = parentBottom - textHeight - (int) (parentHeight * bottomPaddingFraction); } - textWidth = textRight - textLeft; - // Update the derived drawing variables. this.textLayout = new StaticLayout(cueText, textPaint, textWidth, textAlignment, spacingMult, spacingAdd, true); From 35988395d2aa7331be9b4d5640156bac28b4c83a Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 27 Feb 2017 03:49:44 -0800 Subject: [PATCH 084/140] Fix incorrect Javadoc ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148633347 --- .../java/com/google/android/exoplayer2/extractor/mp4/Track.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java index d673564dc4..f1c4e99ec1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java @@ -93,7 +93,7 @@ public final class Track { public final long[] editListMediaTimes; /** - * For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. -1 for + * For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. 0 for * other track types. */ public final int nalUnitLengthFieldLength; From e26723cdc7cce0bbe0363db02525d5dfbeeb0011 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 27 Feb 2017 04:40:48 -0800 Subject: [PATCH 085/140] Add MODE_SINGLE_PMT to TsExtractor This mode allows the extractor to support streams with multiple programs declared in the PAT, but only one PMT. This is necessary to support tuner-obtained media. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148636312 --- .../extractor/ts/TsExtractorTest.java | 6 +- .../exoplayer2/extractor/ts/TsExtractor.java | 58 +++++++++++++------ .../exoplayer2/source/hls/HlsMediaChunk.java | 4 +- 3 files changed, 47 insertions(+), 21 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index 9bcb1c2377..7bf722cd8f 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -75,7 +75,8 @@ public final class TsExtractorTest extends InstrumentationTestCase { public void testCustomPesReader() throws Exception { CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(true, false); - TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory, false); + TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0), + factory); FakeExtractorInput input = new FakeExtractorInput.Builder() .setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts")) .setSimulateIOErrors(false) @@ -99,7 +100,8 @@ public final class TsExtractorTest extends InstrumentationTestCase { public void testCustomInitialSectionReader() throws Exception { CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(false, true); - TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory, false); + TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0), + factory); FakeExtractorInput input = new FakeExtractorInput.Builder() .setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample_with_sdt.ts")) .setSimulateIOErrors(false) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 99f5d0832e..ca3ea7ce39 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ts; +import android.support.annotation.IntDef; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; @@ -34,6 +35,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -56,6 +59,27 @@ public final class TsExtractor implements Extractor { }; + /** + * Modes for the extractor. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({MODE_NORMAL, MODE_SINGLE_PMT, MODE_HLS}) + public @interface Mode {} + + /** + * Behave as defined in ISO/IEC 13818-1. + */ + public static final int MODE_NORMAL = 0; + /** + * Assume only one PMT will be contained in the stream, even if more are declared by the PAT. + */ + public static final int MODE_SINGLE_PMT = 1; + /** + * Enable single PMT mode, map {@link TrackOutput}s by their type (instead of PID) and ignore + * continuity counters. + */ + public static final int MODE_HLS = 2; + public static final int TS_STREAM_TYPE_MPA = 0x03; public static final int TS_STREAM_TYPE_MPA_LSF = 0x04; public static final int TS_STREAM_TYPE_AAC = 0x0F; @@ -81,7 +105,7 @@ public final class TsExtractor implements Extractor { private static final int BUFFER_PACKET_COUNT = 5; // Should be at least 2 private static final int BUFFER_SIZE = TS_PACKET_SIZE * BUFFER_PACKET_COUNT; - private final boolean hlsMode; + @Mode private final int mode; private final List timestampAdjusters; private final ParsableByteArray tsPacketBuffer; private final ParsableBitArray tsScratch; @@ -97,25 +121,25 @@ public final class TsExtractor implements Extractor { private TsPayloadReader id3Reader; public TsExtractor() { - this(new TimestampAdjuster(0), new DefaultTsPayloadReaderFactory(), false); + this(MODE_NORMAL, new TimestampAdjuster(0), new DefaultTsPayloadReaderFactory()); } /** + * @param mode Mode for the extractor. One of {@link #MODE_NORMAL}, {@link #MODE_SINGLE_PMT} + * and {@link #MODE_HLS}. * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. * @param payloadReaderFactory Factory for injecting a custom set of payload readers. - * @param hlsMode Whether the extractor should be used in HLS mode. If true, {@link TrackOutput}s - * are mapped by their type (instead of PID) and continuity counters are ignored. */ - public TsExtractor(TimestampAdjuster timestampAdjuster, - TsPayloadReader.Factory payloadReaderFactory, boolean hlsMode) { - if (hlsMode) { + public TsExtractor(@Mode int mode, TimestampAdjuster timestampAdjuster, + TsPayloadReader.Factory payloadReaderFactory) { + this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory); + this.mode = mode; + if (mode == MODE_SINGLE_PMT || mode == MODE_HLS) { timestampAdjusters = Collections.singletonList(timestampAdjuster); } else { timestampAdjusters = new ArrayList<>(); timestampAdjusters.add(timestampAdjuster); } - this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory); - this.hlsMode = hlsMode; tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE); tsScratch = new ParsableBitArray(new byte[3]); trackIds = new SparseBooleanArray(); @@ -220,7 +244,7 @@ public final class TsExtractor implements Extractor { // Discontinuity check. boolean discontinuityFound = false; int continuityCounter = tsScratch.readBits(4); - if (!hlsMode) { + if (mode != MODE_HLS) { int previousCounter = continuityCounters.get(pid, continuityCounter - 1); continuityCounters.put(pid, continuityCounter); if (previousCounter == continuityCounter) { @@ -315,7 +339,7 @@ public final class TsExtractor implements Extractor { remainingPmts++; } } - if (!hlsMode) { + if (mode != MODE_HLS) { tsPayloadReaders.remove(TS_PAT_PID); } } @@ -356,7 +380,7 @@ public final class TsExtractor implements Extractor { } // TimestampAdjuster assignment. TimestampAdjuster timestampAdjuster; - if (hlsMode || remainingPmts == 1) { + if (mode == MODE_SINGLE_PMT || mode == MODE_HLS || remainingPmts == 1) { timestampAdjuster = timestampAdjusters.get(0); } else { timestampAdjuster = new TimestampAdjuster(timestampAdjusters.get(0).firstSampleTimestampUs); @@ -378,7 +402,7 @@ public final class TsExtractor implements Extractor { // Skip the descriptors. sectionData.skipBytes(programInfoLength); - if (hlsMode && id3Reader == null) { + if (mode == MODE_HLS && id3Reader == null) { // Setup an ID3 track regardless of whether there's a corresponding entry, in case one // appears intermittently during playback. See [Internal: b/20261500]. EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, new byte[0]); @@ -401,14 +425,14 @@ public final class TsExtractor implements Extractor { } remainingEntriesLength -= esInfoLength + 5; - int trackId = hlsMode ? streamType : elementaryPid; + int trackId = mode == MODE_HLS ? streamType : elementaryPid; if (trackIds.get(trackId)) { continue; } trackIds.put(trackId, true); TsPayloadReader reader; - if (hlsMode && streamType == TS_STREAM_TYPE_ID3) { + if (mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3) { reader = id3Reader; } else { reader = payloadReaderFactory.createPayloadReader(streamType, esInfo); @@ -422,7 +446,7 @@ public final class TsExtractor implements Extractor { tsPayloadReaders.put(elementaryPid, reader); } } - if (hlsMode) { + if (mode == MODE_HLS) { if (!tracksEnded) { output.endTracks(); remainingPmts = 0; @@ -430,7 +454,7 @@ public final class TsExtractor implements Extractor { } } else { tsPayloadReaders.remove(pid); - remainingPmts--; + remainingPmts = mode == MODE_SINGLE_PMT ? 0 : remainingPmts - 1; if (remainingPmts == 0) { output.endTracks(); tracksEnded = true; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index cc3b1087e4..5fdc1f4e32 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -372,8 +372,8 @@ import java.util.concurrent.atomic.AtomicInteger; esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM; } } - extractor = new TsExtractor(timestampAdjuster, - new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, muxedCaptionFormats), true); + extractor = new TsExtractor(TsExtractor.MODE_HLS, timestampAdjuster, + new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, muxedCaptionFormats)); } if (usingNewExtractor) { extractor.init(extractorOutput); From 98f4fb85c210b2704a66e00c8477d4a41a234e9c Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 27 Feb 2017 07:36:11 -0800 Subject: [PATCH 086/140] Move utility methods to DashUtil class ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148647040 --- .../exoplayer2/drm/OfflineLicenseHelper.java | 69 +------- .../exoplayer2/source/dash/DashUtil.java | 166 ++++++++++++++++++ 2 files changed, 169 insertions(+), 66 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index b3729c2377..6a7f905a51 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.drm; import android.media.MediaDrm; -import android.net.Uri; import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; @@ -27,24 +26,14 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.EventListener; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.Mode; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; -import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; -import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; -import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper; -import com.google.android.exoplayer2.source.chunk.InitializationChunk; +import com.google.android.exoplayer2.source.dash.DashUtil; import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; -import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.dash.manifest.Period; -import com.google.android.exoplayer2.source.dash.manifest.RangedUri; import com.google.android.exoplayer2.source.dash.manifest.Representation; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DataSourceInputStream; -import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; import java.util.HashMap; @@ -58,28 +47,6 @@ public final class OfflineLicenseHelper { private final DefaultDrmSessionManager drmSessionManager; private final HandlerThread handlerThread; - /** - * Helper method to download a DASH manifest. - * - * @param dataSource The {@link HttpDataSource} from which the manifest should be read. - * @param manifestUriString The URI of the manifest to be read. - * @return An instance of {@link DashManifest}. - * @throws IOException If an error occurs reading data from the stream. - * @see DashManifestParser - */ - public static DashManifest downloadManifest(HttpDataSource dataSource, String manifestUriString) - throws IOException { - DataSourceInputStream inputStream = new DataSourceInputStream( - dataSource, new DataSpec(Uri.parse(manifestUriString))); - try { - inputStream.open(); - DashManifestParser parser = new DashManifestParser(); - return parser.parse(dataSource.getUri(), inputStream); - } finally { - inputStream.close(); - } - } - /** * Instantiates a new instance which uses Widevine CDM. Call {@link #releaseResources()} when * you're done with the helper instance. @@ -174,7 +141,7 @@ public final class OfflineLicenseHelper { */ public byte[] download(HttpDataSource dataSource, String manifestUriString) throws IOException, InterruptedException, DrmSessionException { - return download(dataSource, downloadManifest(dataSource, manifestUriString)); + return download(dataSource, DashUtil.loadManifest(dataSource, manifestUriString)); } /** @@ -210,14 +177,8 @@ public final class OfflineLicenseHelper { Representation representation = adaptationSet.representations.get(0); DrmInitData drmInitData = representation.format.drmInitData; if (drmInitData == null) { - ChunkExtractorWrapper extractorWrapper = newWrappedExtractor(representation.format, + Format sampleFormat = DashUtil.loadSampleFormat(dataSource, representation, adaptationSet.type); - InitializationChunk initializationChunk = loadInitializationChunk(dataSource, representation, - extractorWrapper); - if (initializationChunk == null) { - return null; - } - Format sampleFormat = extractorWrapper.getSampleFormat(); if (sampleFormat != null) { drmInitData = sampleFormat.drmInitData; } @@ -291,28 +252,4 @@ public final class OfflineLicenseHelper { return session; } - private static InitializationChunk loadInitializationChunk(DataSource dataSource, - Representation representation, ChunkExtractorWrapper extractorWrapper) - throws IOException, InterruptedException { - RangedUri rangedUri = representation.getInitializationUri(); - if (rangedUri == null) { - return null; - } - DataSpec dataSpec = new DataSpec(rangedUri.resolveUri(representation.baseUrl), rangedUri.start, - rangedUri.length, representation.getCacheKey()); - InitializationChunk initializationChunk = new InitializationChunk(dataSource, dataSpec, - representation.format, C.SELECTION_REASON_UNKNOWN, null /* trackSelectionData */, - extractorWrapper); - initializationChunk.load(); - return initializationChunk; - } - - private static ChunkExtractorWrapper newWrappedExtractor(Format format, int trackType) { - final String mimeType = format.containerMimeType; - final boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM) - || mimeType.startsWith(MimeTypes.AUDIO_WEBM); - final Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor(); - return new ChunkExtractorWrapper(extractor, format, trackType); - } - } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java new file mode 100644 index 0000000000..bc8d67816f --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2017 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.source.dash; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ChunkIndex; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; +import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; +import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper; +import com.google.android.exoplayer2.source.chunk.InitializationChunk; +import com.google.android.exoplayer2.source.dash.manifest.DashManifest; +import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; +import com.google.android.exoplayer2.source.dash.manifest.RangedUri; +import com.google.android.exoplayer2.source.dash.manifest.Representation; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSourceInputStream; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.util.MimeTypes; +import java.io.IOException; + +/** + * Utility methods for DASH streams. + */ +public final class DashUtil { + + /** + * Loads a DASH manifest. + * + * @param dataSource The {@link HttpDataSource} from which the manifest should be read. + * @param manifestUriString The URI of the manifest to be read. + * @return An instance of {@link DashManifest}. + * @throws IOException If an error occurs reading data from the stream. + * @see DashManifestParser + */ + public static DashManifest loadManifest(DataSource dataSource, String manifestUriString) + throws IOException { + DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, + new DataSpec(Uri.parse(manifestUriString), DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH)); + try { + inputStream.open(); + DashManifestParser parser = new DashManifestParser(); + return parser.parse(dataSource.getUri(), inputStream); + } finally { + inputStream.close(); + } + } + + /** + * Loads initialization data for the {@code representation} and returns the sample {@link + * Format}. + * + * @param dataSource The source from which the data should be loaded. + * @param representation The representation which initialization chunk belongs to. + * @param type The type of the primary track. Typically one of the {@link C} {@code TRACK_TYPE_*} + * constants. + * @return the sample {@link Format} of the given representation. + * @throws IOException Thrown when there is an error while loading. + * @throws InterruptedException Thrown if the thread was interrupted. + */ + public static Format loadSampleFormat(DataSource dataSource, Representation representation, + int type) throws IOException, InterruptedException { + ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, representation, + type, false); + return extractorWrapper == null ? null : extractorWrapper.getSampleFormat(); + } + + /** + * Loads initialization and index data for the {@code representation} and returns the {@link + * ChunkIndex}. + * + * @param dataSource The source from which the data should be loaded. + * @param representation The representation which initialization chunk belongs to. + * @param type The type of the primary track. Typically one of the {@link C} {@code TRACK_TYPE_*} + * constants. + * @return {@link ChunkIndex} of the given representation. + * @throws IOException Thrown when there is an error while loading. + * @throws InterruptedException Thrown if the thread was interrupted. + */ + public static ChunkIndex loadChunkIndex(DataSource dataSource, Representation representation, + int type) throws IOException, InterruptedException { + ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, representation, + type, true); + return extractorWrapper == null ? null : (ChunkIndex) extractorWrapper.getSeekMap(); + } + + /** + * Loads initialization data for the {@code representation} and optionally index data then + * returns a {@link ChunkExtractorWrapper} which contains the output. + * + * @param dataSource The source from which the data should be loaded. + * @param representation The representation which initialization chunk belongs to. + * @param type The type of the primary track. Typically one of the {@link C} {@code TRACK_TYPE_*} + * constants. + * @param loadIndex Whether to load index data too. + * @return A {@link ChunkExtractorWrapper} for the {@code representation}, or null if no + * initialization or (if requested) index data exists. + * @throws IOException Thrown when there is an error while loading. + * @throws InterruptedException Thrown if the thread was interrupted. + */ + private static ChunkExtractorWrapper loadInitializationData(DataSource dataSource, + Representation representation, int type, boolean loadIndex) + throws IOException, InterruptedException { + RangedUri initializationUri = representation.getInitializationUri(); + if (initializationUri == null) { + return null; + } + ChunkExtractorWrapper extractorWrapper = newWrappedExtractor(representation.format, type); + RangedUri requestUri; + if (loadIndex) { + RangedUri indexUri = representation.getIndexUri(); + if (indexUri == null) { + return null; + } + // It's common for initialization and index data to be stored adjacently. Attempt to merge + // the two requests together to request both at once. + requestUri = initializationUri.attemptMerge(indexUri, representation.baseUrl); + if (requestUri == null) { + loadInitializationData(dataSource, representation, extractorWrapper, initializationUri); + requestUri = indexUri; + } + } else { + requestUri = initializationUri; + } + loadInitializationData(dataSource, representation, extractorWrapper, requestUri); + return extractorWrapper; + } + + private static void loadInitializationData(DataSource dataSource, + Representation representation, ChunkExtractorWrapper extractorWrapper, RangedUri requestUri) + throws IOException, InterruptedException { + DataSpec dataSpec = new DataSpec(requestUri.resolveUri(representation.baseUrl), + requestUri.start, requestUri.length, representation.getCacheKey()); + InitializationChunk initializationChunk = new InitializationChunk(dataSource, dataSpec, + representation.format, C.SELECTION_REASON_UNKNOWN, null /* trackSelectionData */, + extractorWrapper); + initializationChunk.load(); + } + + private static ChunkExtractorWrapper newWrappedExtractor(Format format, int trackType) { + String mimeType = format.containerMimeType; + boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM) + || mimeType.startsWith(MimeTypes.AUDIO_WEBM); + Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor(); + return new ChunkExtractorWrapper(extractor, format, trackType); + } + + private DashUtil() {} + +} From 2411d0fc7697e53897bb74c6575a46277a7e2788 Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 27 Feb 2017 08:25:54 -0800 Subject: [PATCH 087/140] Move and rename PRIORITY_PLAYBACK to C constants class ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148651265 --- .../main/java/com/google/android/exoplayer2/C.java | 5 +++++ .../android/exoplayer2/DefaultLoadControl.java | 13 ++++--------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/C.java b/library/src/main/java/com/google/android/exoplayer2/C.java index 4f3e462dec..ec7e6fa3de 100644 --- a/library/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/src/main/java/com/google/android/exoplayer2/C.java @@ -548,6 +548,11 @@ public final class C { */ public static final int STEREO_MODE_STEREO_MESH = 3; + /** + * Priority for media playback. + */ + public static final int PRIORITY_PLAYBACK = 0; + /** * Converts a time in microseconds to the corresponding time in milliseconds, preserving * {@link #TIME_UNSET} values. diff --git a/library/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index fe7015a942..d8bc042ad7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -51,11 +51,6 @@ public final class DefaultLoadControl implements LoadControl { */ public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; - /** - * Priority for media loading. - */ - public static final int LOADING_PRIORITY = 0; - private static final int ABOVE_HIGH_WATERMARK = 0; private static final int BETWEEN_WATERMARKS = 1; private static final int BELOW_LOW_WATERMARK = 2; @@ -122,7 +117,7 @@ public final class DefaultLoadControl implements LoadControl { * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by * buffer depletion rather than a user action. * @param priorityTaskManager If not null, registers itself as a task with priority - * {@link #LOADING_PRIORITY} during loading periods, and unregisters itself during draining + * {@link C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining * periods. */ public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, @@ -183,9 +178,9 @@ public final class DefaultLoadControl implements LoadControl { || (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached); if (priorityTaskManager != null && isBuffering != wasBuffering) { if (isBuffering) { - priorityTaskManager.add(LOADING_PRIORITY); + priorityTaskManager.add(C.PRIORITY_PLAYBACK); } else { - priorityTaskManager.remove(LOADING_PRIORITY); + priorityTaskManager.remove(C.PRIORITY_PLAYBACK); } } return isBuffering; @@ -199,7 +194,7 @@ public final class DefaultLoadControl implements LoadControl { private void reset(boolean resetAllocator) { targetBufferSize = 0; if (priorityTaskManager != null && isBuffering) { - priorityTaskManager.remove(LOADING_PRIORITY); + priorityTaskManager.remove(C.PRIORITY_PLAYBACK); } isBuffering = false; if (resetAllocator) { From b3cfeaa17b6203c738df8e85b0868ddaebcfe3cb Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 27 Feb 2017 09:00:48 -0800 Subject: [PATCH 088/140] Discard extra silent channels on Samsung Galaxy S6/S7. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148654495 --- .../android/exoplayer2/audio/AudioTrack.java | 37 ++++- .../exoplayer2/audio/BufferProcessor.java | 14 +- .../audio/ChannelMappingBufferProcessor.java | 155 ++++++++++++++++++ .../audio/MediaCodecAudioRenderer.java | 30 +++- .../audio/ResamplingBufferProcessor.java | 10 +- 5 files changed, 230 insertions(+), 16 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingBufferProcessor.java diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index 2f343ec40e..76b5ec72fe 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -270,6 +270,7 @@ public final class AudioTrack { public static boolean failOnSpuriousAudioTimestamp = false; private final AudioCapabilities audioCapabilities; + private final ChannelMappingBufferProcessor channelMappingBufferProcessor; private final BufferProcessor[] availableBufferProcessors; private final Listener listener; private final ConditionVariable releasingConditionVariable; @@ -343,9 +344,11 @@ public final class AudioTrack { public AudioTrack(AudioCapabilities audioCapabilities, BufferProcessor[] bufferProcessors, Listener listener) { this.audioCapabilities = audioCapabilities; - availableBufferProcessors = new BufferProcessor[bufferProcessors.length + 1]; + channelMappingBufferProcessor = new ChannelMappingBufferProcessor(); + availableBufferProcessors = new BufferProcessor[bufferProcessors.length + 2]; availableBufferProcessors[0] = new ResamplingBufferProcessor(); - System.arraycopy(bufferProcessors, 0, availableBufferProcessors, 1, bufferProcessors.length); + availableBufferProcessors[1] = channelMappingBufferProcessor; + System.arraycopy(bufferProcessors, 0, availableBufferProcessors, 2, bufferProcessors.length); this.listener = listener; releasingConditionVariable = new ConditionVariable(true); if (Util.SDK_INT >= 18) { @@ -449,6 +452,30 @@ public final class AudioTrack { */ public void configure(String mimeType, int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding, int specifiedBufferSize) throws ConfigurationException { + configure(mimeType, channelCount, sampleRate, pcmEncoding, specifiedBufferSize, null); + } + + /** + * Configures (or reconfigures) the audio track. + * + * @param mimeType The mime type. + * @param channelCount The number of channels. + * @param sampleRate The sample rate in Hz. + * @param pcmEncoding For PCM formats, the encoding used. One of {@link C#ENCODING_PCM_16BIT}, + * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and + * {@link C#ENCODING_PCM_32BIT}. + * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a + * suitable buffer size automatically. + * @param outputChannels A mapping from input to output channels that is applied to this track's + * input as a preprocessing step, if handling PCM input. Specify {@code null} to leave the + * input unchanged. Otherwise, the element at index {@code i} specifies index of the input + * channel to map to output channel {@code i} when preprocessing input buffers. After the + * map is applied the audio data will have {@code outputChannels.length} channels. + * @throws ConfigurationException If an error occurs configuring the track. + */ + public void configure(String mimeType, int channelCount, int sampleRate, + @C.PcmEncoding int pcmEncoding, int specifiedBufferSize, int[] outputChannels) + throws ConfigurationException { boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); @C.Encoding int encoding = passthrough ? getEncodingForMimeType(mimeType) : pcmEncoding; boolean flush = false; @@ -456,17 +483,15 @@ public final class AudioTrack { pcmFrameSize = Util.getPcmFrameSize(pcmEncoding, channelCount); // Reconfigure the buffer processors. + channelMappingBufferProcessor.setChannelMap(outputChannels); ArrayList newBufferProcessors = new ArrayList<>(); for (BufferProcessor bufferProcessor : availableBufferProcessors) { - boolean wasActive = bufferProcessor.isActive(); try { flush |= bufferProcessor.configure(sampleRate, channelCount, encoding); } catch (BufferProcessor.UnhandledFormatException e) { throw new ConfigurationException(e); } - boolean isActive = bufferProcessor.isActive(); - flush |= isActive != wasActive; - if (isActive) { + if (bufferProcessor.isActive()) { newBufferProcessors.add(bufferProcessor); channelCount = bufferProcessor.getOutputChannelCount(); encoding = bufferProcessor.getOutputEncoding(); diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java index c31823fd3b..87d4e5fe7b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java @@ -42,16 +42,18 @@ public interface BufferProcessor { ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder()); /** - * Configures the processor to process input buffers with the specified format and returns whether - * the processor must be flushed. After calling this method, {@link #isActive()} returns whether - * the processor needs to handle buffers; if not, the processor will not accept any buffers until - * it is reconfigured. {@link #getOutputChannelCount()} and {@link #getOutputEncoding()} return - * the processor's output format. + * Configures the processor to process input buffers with the specified format. After calling this + * method, {@link #isActive()} returns whether the processor needs to handle buffers; if not, the + * processor will not accept any buffers until it is reconfigured. Returns {@code true} if the + * processor must be flushed, or if the value returned by {@link #isActive()} has changed as a + * result of the call. If it's active, {@link #getOutputChannelCount()} and + * {@link #getOutputEncoding()} return the processor's output format. * * @param sampleRateHz The sample rate of input audio in Hz. * @param channelCount The number of interleaved channels in input audio. * @param encoding The encoding of input audio. - * @return Whether the processor must be flushed. + * @return {@code true} if the processor must be flushed or the value returned by + * {@link #isActive()} has changed as a result of the call. * @throws UnhandledFormatException Thrown if the specified format can't be handled as input. */ boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingBufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingBufferProcessor.java new file mode 100644 index 0000000000..8c23198925 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingBufferProcessor.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2017 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.audio; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.C.Encoding; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +/** + * Buffer processor that applies a mapping from input channels onto specified output channels. This + * can be used to reorder, duplicate or discard channels. + */ +/* package */ final class ChannelMappingBufferProcessor implements BufferProcessor { + + private int channelCount; + private int sampleRateHz; + private int[] pendingOutputChannels; + + private boolean active; + private int[] outputChannels; + private ByteBuffer buffer; + private ByteBuffer outputBuffer; + private boolean inputEnded; + + /** + * Creates a new processor that applies a channel mapping. + */ + public ChannelMappingBufferProcessor() { + buffer = EMPTY_BUFFER; + outputBuffer = EMPTY_BUFFER; + } + + /** + * Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)} + * to start using the new channel map. + * + * @see AudioTrack#configure(String, int, int, int, int, int[]) + */ + public void setChannelMap(int[] outputChannels) { + pendingOutputChannels = outputChannels; + } + + @Override + public boolean configure(int sampleRateHz, int channelCount, @Encoding int encoding) + throws UnhandledFormatException { + boolean outputChannelsChanged = !Arrays.equals(pendingOutputChannels, outputChannels); + outputChannels = pendingOutputChannels; + if (outputChannels == null) { + active = false; + return outputChannelsChanged; + } + if (encoding != C.ENCODING_PCM_16BIT) { + throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + if (!outputChannelsChanged && this.sampleRateHz == sampleRateHz + && this.channelCount == channelCount) { + return false; + } + this.sampleRateHz = sampleRateHz; + this.channelCount = channelCount; + + active = channelCount != outputChannels.length; + for (int i = 0; i < outputChannels.length; i++) { + int channelIndex = outputChannels[i]; + if (channelIndex >= channelCount) { + throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + active |= (channelIndex != i); + } + return true; + } + + @Override + public boolean isActive() { + return active; + } + + @Override + public int getOutputChannelCount() { + return outputChannels == null ? channelCount : outputChannels.length; + } + + @Override + public int getOutputEncoding() { + return C.ENCODING_PCM_16BIT; + } + + @Override + public void queueInput(ByteBuffer inputBuffer) { + int position = inputBuffer.position(); + int limit = inputBuffer.limit(); + int frameCount = (limit - position) / (2 * channelCount); + int outputSize = frameCount * outputChannels.length * 2; + if (buffer.capacity() < outputSize) { + buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder()); + } else { + buffer.clear(); + } + while (position < limit) { + for (int channelIndex : outputChannels) { + buffer.putShort(inputBuffer.getShort(position + 2 * channelIndex)); + } + position += channelCount * 2; + } + inputBuffer.position(limit); + buffer.flip(); + outputBuffer = buffer; + } + + @Override + public void queueEndOfStream() { + inputEnded = true; + } + + @Override + public ByteBuffer getOutput() { + ByteBuffer outputBuffer = this.outputBuffer; + this.outputBuffer = EMPTY_BUFFER; + return outputBuffer; + } + + @SuppressWarnings("ReferenceEquality") + @Override + public boolean isEnded() { + return inputEnded && outputBuffer == EMPTY_BUFFER; + } + + @Override + public void flush() { + outputBuffer = EMPTY_BUFFER; + inputEnded = false; + } + + @Override + public void release() { + flush(); + buffer = EMPTY_BUFFER; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 7ab9d9133a..76f7ac08bb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -47,8 +47,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private final AudioTrack audioTrack; private boolean passthroughEnabled; + private boolean codecNeedsDiscardChannelsWorkaround; private android.media.MediaFormat passthroughMediaFormat; private int pcmEncoding; + private int channelCount; private long currentPositionUs; private boolean allowPositionDiscontinuity; @@ -188,6 +190,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, MediaCrypto crypto) { + codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name); if (passthroughEnabled) { // Override the MIME type used to configure the codec if we are using a passthrough decoder. passthroughMediaFormat = format.getFrameworkMediaFormatV16(); @@ -219,6 +222,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media // output 16-bit PCM. pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding : C.ENCODING_PCM_16BIT; + channelCount = newFormat.channelCount; } @Override @@ -230,8 +234,18 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat; int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); + int[] channelMap; + if (codecNeedsDiscardChannelsWorkaround && channelCount == 6 && this.channelCount < 6) { + channelMap = new int[this.channelCount]; + for (int i = 0; i < this.channelCount; i++) { + channelMap[i] = i; + } + } else { + channelMap = null; + } + try { - audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0); + audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0, channelMap); } catch (AudioTrack.ConfigurationException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } @@ -388,6 +402,20 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } + /** + * Returns whether the decoder is known to output six audio channels when provided with input with + * fewer than six channels. + *

      + * See [Internal: b/35655036]. + */ + private static boolean codecNeedsDiscardChannelsWorkaround(String codecName) { + // The workaround applies to Samsung Galaxy S6 and Samsung Galaxy S7. + return Util.SDK_INT < 24 && "OMX.SEC.aac.dec".equals(codecName) + && "samsung".equals(Util.MANUFACTURER) + && (Util.DEVICE.startsWith("zeroflte") || Util.DEVICE.startsWith("herolte") + || Util.DEVICE.startsWith("heroqlte")); + } + private final class AudioTrackListener implements AudioTrack.Listener { @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java index 9d06c4718f..370e54c58d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java @@ -25,6 +25,7 @@ import java.nio.ByteOrder; */ /* package */ final class ResamplingBufferProcessor implements BufferProcessor { + private int sampleRateHz; private int channelCount; @C.PcmEncoding private int encoding; @@ -36,6 +37,8 @@ import java.nio.ByteOrder; * Creates a new buffer processor that converts audio data to {@link C#ENCODING_PCM_16BIT}. */ public ResamplingBufferProcessor() { + sampleRateHz = Format.NO_VALUE; + channelCount = Format.NO_VALUE; encoding = C.ENCODING_INVALID; buffer = EMPTY_BUFFER; outputBuffer = EMPTY_BUFFER; @@ -48,11 +51,12 @@ import java.nio.ByteOrder; && encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) { throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); } - this.channelCount = channelCount; - if (this.encoding == encoding) { + if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount + && this.encoding == encoding) { return false; } - + this.sampleRateHz = sampleRateHz; + this.channelCount = channelCount; this.encoding = encoding; if (encoding == C.ENCODING_PCM_16BIT) { buffer = EMPTY_BUFFER; From 1120e1027315687b3cca0de024c0d59fce2793a4 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 27 Feb 2017 09:42:03 -0800 Subject: [PATCH 089/140] Add GVR spatial audio rendering extension. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148658631 --- extensions/gvr/src/main/AndroidManifest.xml | 16 ++ .../ext/gvr/GvrBufferProcessor.java | 170 ++++++++++++++++++ settings.gradle | 2 + 3 files changed, 188 insertions(+) create mode 100644 extensions/gvr/src/main/AndroidManifest.xml create mode 100644 extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrBufferProcessor.java diff --git a/extensions/gvr/src/main/AndroidManifest.xml b/extensions/gvr/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..6706b2507e --- /dev/null +++ b/extensions/gvr/src/main/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrBufferProcessor.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrBufferProcessor.java new file mode 100644 index 0000000000..fa94539c31 --- /dev/null +++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrBufferProcessor.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2017 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.ext.gvr; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.audio.BufferProcessor; +import com.google.vr.sdk.audio.GvrAudioSurround; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Buffer processor that uses {@code GvrAudioSurround} to provide binaural rendering of surround + * sound and ambisonic soundfields. + */ +public final class GvrBufferProcessor implements BufferProcessor { + + private static final int FRAMES_PER_OUTPUT_BUFFER = 1024; + private static final int OUTPUT_CHANNEL_COUNT = 2; + private static final int OUTPUT_FRAME_SIZE = OUTPUT_CHANNEL_COUNT * 2; // 16-bit stereo output. + + private int sampleRateHz; + private int channelCount; + private GvrAudioSurround gvrAudioSurround; + private ByteBuffer buffer; + private boolean inputEnded; + + private float w; + private float x; + private float y; + private float z; + + public GvrBufferProcessor() { + // Use the identity for the initial orientation. + w = 1f; + sampleRateHz = Format.NO_VALUE; + channelCount = Format.NO_VALUE; + } + + /** + * Updates the listener head orientation. May be called on any thread. See + * {@code GvrAudioSurround.updateNativeOrientation}. + */ + public synchronized void updateOrientation(float w, float x, float y, float z) { + this.w = w; + this.x = x; + this.y = y; + this.z = z; + if (gvrAudioSurround != null) { + gvrAudioSurround.updateNativeOrientation(w, x, y, z); + } + } + + @Override + public synchronized boolean configure(int sampleRateHz, int channelCount, + @C.Encoding int encoding) throws UnhandledFormatException { + if (encoding != C.ENCODING_PCM_16BIT) { + maybeReleaseGvrAudioSurround(); + throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount) { + return false; + } + this.sampleRateHz = sampleRateHz; + this.channelCount = channelCount; + maybeReleaseGvrAudioSurround(); + int surroundFormat; + switch (channelCount) { + case 2: + surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO; + break; + case 4: + surroundFormat = GvrAudioSurround.SurroundFormat.FIRST_ORDER_AMBISONICS; + break; + case 6: + surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_FIVE_DOT_ONE; + break; + case 9: + surroundFormat = GvrAudioSurround.SurroundFormat.SECOND_ORDER_AMBISONICS; + break; + case 16: + surroundFormat = GvrAudioSurround.SurroundFormat.THIRD_ORDER_AMBISONICS; + break; + default: + throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + gvrAudioSurround = new GvrAudioSurround(surroundFormat, sampleRateHz, channelCount, + FRAMES_PER_OUTPUT_BUFFER); + gvrAudioSurround.updateNativeOrientation(w, x, y, z); + if (buffer == null) { + buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE) + .order(ByteOrder.nativeOrder()); + } + return true; + } + + @Override + public boolean isActive() { + return gvrAudioSurround != null; + } + + @Override + public int getOutputChannelCount() { + return OUTPUT_CHANNEL_COUNT; + } + + @Override + public int getOutputEncoding() { + return C.ENCODING_PCM_16BIT; + } + + @Override + public void queueInput(ByteBuffer input) { + int position = input.position(); + int readBytes = gvrAudioSurround.addInput(input, position, input.limit() - position); + input.position(position + readBytes); + } + + @Override + public void queueEndOfStream() { + inputEnded = true; + gvrAudioSurround.triggerProcessing(); + } + + @Override + public ByteBuffer getOutput() { + int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity()); + buffer.position(0).limit(writtenBytes); + return buffer; + } + + @Override + public boolean isEnded() { + return inputEnded && gvrAudioSurround.getAvailableOutputSize() == 0; + } + + @Override + public void flush() { + gvrAudioSurround.flush(); + inputEnded = false; + } + + @Override + public synchronized void release() { + buffer = null; + maybeReleaseGvrAudioSurround(); + } + + private void maybeReleaseGvrAudioSurround() { + if (this.gvrAudioSurround != null) { + GvrAudioSurround gvrAudioSurround = this.gvrAudioSurround; + this.gvrAudioSurround = null; + gvrAudioSurround.release(); + } + } + +} diff --git a/settings.gradle b/settings.gradle index d2d9cc87ed..b69c134fc4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,6 +17,7 @@ include ':demo' include ':playbacktests' include ':extension-ffmpeg' include ':extension-flac' +include ':extension-gvr' include ':extension-okhttp' include ':extension-opus' include ':extension-vp9' @@ -25,6 +26,7 @@ include ':extension-vp9' project(':extension-ffmpeg').projectDir = new File(settingsDir, 'extensions/ffmpeg') project(':extension-flac').projectDir = new File(settingsDir, 'extensions/flac') +project(':extension-gvr').projectDir = new File(settingsDir, 'extensions/gvr') project(':extension-okhttp').projectDir = new File(settingsDir, 'extensions/okhttp') project(':extension-opus').projectDir = new File(settingsDir, 'extensions/opus') project(':extension-vp9').projectDir = new File(settingsDir, 'extensions/vp9') From 91639b26cd40d25094e1539417268fd14214d0d8 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 28 Feb 2017 05:59:49 -0800 Subject: [PATCH 090/140] Add some pipelining in MetadataRenderer ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148760782 --- .../exoplayer2/metadata/MetadataRenderer.java | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index 6c2ef319fd..814238970b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.util.Assertions; +import java.util.Arrays; /** * A renderer for metadata. @@ -46,17 +47,23 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } private static final int MSG_INVOKE_RENDERER = 0; + // TODO: Holding multiple pending metadata objects is temporary mitigation against + // https://github.com/google/ExoPlayer/issues/1874 + // It should be removed once this issue has been addressed. + private static final int MAX_PENDING_METADATA_COUNT = 5; private final MetadataDecoderFactory decoderFactory; private final Output output; private final Handler outputHandler; private final FormatHolder formatHolder; private final MetadataInputBuffer buffer; + private final Metadata[] pendingMetadata; + private final long[] pendingMetadataTimestamps; + private int pendingMetadataIndex; + private int pendingMetadataCount; private MetadataDecoder decoder; private boolean inputStreamEnded; - private long pendingMetadataTimestamp; - private Metadata pendingMetadata; /** * @param output The output. @@ -87,6 +94,8 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { this.decoderFactory = Assertions.checkNotNull(decoderFactory); formatHolder = new FormatHolder(); buffer = new MetadataInputBuffer(); + pendingMetadata = new Metadata[MAX_PENDING_METADATA_COUNT]; + pendingMetadataTimestamps = new long[MAX_PENDING_METADATA_COUNT]; } @Override @@ -101,13 +110,13 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { @Override protected void onPositionReset(long positionUs, boolean joining) { - pendingMetadata = null; + flushPendingMetadata(); inputStreamEnded = false; } @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - if (!inputStreamEnded && pendingMetadata == null) { + if (!inputStreamEnded && pendingMetadataCount < MAX_PENDING_METADATA_COUNT) { buffer.clear(); int result = readSource(formatHolder, buffer, false); if (result == C.RESULT_BUFFER_READ) { @@ -118,11 +127,13 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { // If we ever need to support a metadata format where this is not the case, we'll need to // pass the buffer to the decoder and discard the output. } else { - pendingMetadataTimestamp = buffer.timeUs; buffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs; buffer.flip(); try { - pendingMetadata = decoder.decode(buffer); + int index = (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT; + pendingMetadata[index] = decoder.decode(buffer); + pendingMetadataTimestamps[index] = buffer.timeUs; + pendingMetadataCount++; } catch (MetadataDecoderException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } @@ -130,15 +141,17 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } } - if (pendingMetadata != null && pendingMetadataTimestamp <= positionUs) { - invokeRenderer(pendingMetadata); - pendingMetadata = null; + if (pendingMetadataCount > 0 && pendingMetadataTimestamps[pendingMetadataIndex] <= positionUs) { + invokeRenderer(pendingMetadata[pendingMetadataIndex]); + pendingMetadata[pendingMetadataIndex] = null; + pendingMetadataIndex = (pendingMetadataIndex + 1) % MAX_PENDING_METADATA_COUNT; + pendingMetadataCount--; } } @Override protected void onDisabled() { - pendingMetadata = null; + flushPendingMetadata(); decoder = null; super.onDisabled(); } @@ -161,6 +174,12 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } } + private void flushPendingMetadata() { + Arrays.fill(pendingMetadata, null); + pendingMetadataIndex = 0; + pendingMetadataCount = 0; + } + @SuppressWarnings("unchecked") @Override public boolean handleMessage(Message msg) { @@ -168,8 +187,10 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { case MSG_INVOKE_RENDERER: invokeRendererInternal((Metadata) msg.obj); return true; + default: + // Should never happen. + throw new IllegalStateException(); } - return false; } private void invokeRendererInternal(Metadata metadata) { From d58008eeb7055fedde8a059b41cb3876a6048d32 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 28 Feb 2017 06:46:51 -0800 Subject: [PATCH 091/140] Rename BufferProcessor to AudioProcessor. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148763781 --- .../ext/ffmpeg/FfmpegAudioRenderer.java | 9 +- .../ext/flac/LibflacAudioRenderer.java | 9 +- extensions/gvr/README.md | 11 +- ...rProcessor.java => GvrAudioProcessor.java} | 10 +- .../ext/opus/LibopusAudioRenderer.java | 16 ++- .../android/exoplayer2/SimpleExoPlayer.java | 31 +++-- ...fferProcessor.java => AudioProcessor.java} | 10 +- .../android/exoplayer2/audio/AudioTrack.java | 114 +++++++++--------- ...java => ChannelMappingAudioProcessor.java} | 8 +- .../audio/MediaCodecAudioRenderer.java | 8 +- ...sor.java => ResamplingAudioProcessor.java} | 8 +- .../audio/SimpleDecoderAudioRenderer.java | 14 +-- 12 files changed, 120 insertions(+), 128 deletions(-) rename extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/{GvrBufferProcessor.java => GvrAudioProcessor.java} (94%) rename library/src/main/java/com/google/android/exoplayer2/audio/{BufferProcessor.java => AudioProcessor.java} (95%) rename library/src/main/java/com/google/android/exoplayer2/audio/{ChannelMappingBufferProcessor.java => ChannelMappingAudioProcessor.java} (93%) rename library/src/main/java/com/google/android/exoplayer2/audio/{ResamplingBufferProcessor.java => ResamplingAudioProcessor.java} (94%) diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java index 6c3ece68a2..8d75ca3dbb 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -19,8 +19,8 @@ import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; -import com.google.android.exoplayer2.audio.BufferProcessor; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; @@ -43,12 +43,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers - * before they are output. + * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - BufferProcessor... bufferProcessors) { - super(eventHandler, eventListener, bufferProcessors); + AudioProcessor... audioProcessors) { + super(eventHandler, eventListener, audioProcessors); } @Override diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java index 5efaf98512..246cde9d2f 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.ext.flac; import android.os.Handler; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; -import com.google.android.exoplayer2.audio.BufferProcessor; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; @@ -38,12 +38,11 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers - * before they are output. + * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - BufferProcessor... bufferProcessors) { - super(eventHandler, eventListener, bufferProcessors); + AudioProcessor... audioProcessors) { + super(eventHandler, eventListener, audioProcessors); } @Override diff --git a/extensions/gvr/README.md b/extensions/gvr/README.md index b6be1705a2..0fe33a6755 100644 --- a/extensions/gvr/README.md +++ b/extensions/gvr/README.md @@ -3,17 +3,16 @@ ## Description ## The GVR extension wraps the [Google VR SDK for Android][]. It provides a -GvrBufferProcessor, which uses [GvrAudioSurround][] to provide binaural -rendering of surround sound and ambisonic soundfields. +GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering +of surround sound and ambisonic soundfields. ## Instructions ## -If using SimpleExoPlayer, override SimpleExoPlayer.buildBufferProcessors to -return a GvrBufferProcessor. +If using SimpleExoPlayer, override SimpleExoPlayer.buildAudioProcessors to +return a GvrAudioProcessor. -If constructing renderers directly, pass a GvrBufferProcessor to +If constructing renderers directly, pass a GvrAudioProcessor to MediaCodecAudioRenderer's constructor. [Google VR SDK for Android]: https://developers.google.com/vr/android/ [GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround - diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrBufferProcessor.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java similarity index 94% rename from extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrBufferProcessor.java rename to extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java index fa94539c31..a53e1c97c5 100644 --- a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrBufferProcessor.java +++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java @@ -17,16 +17,16 @@ package com.google.android.exoplayer2.ext.gvr; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.audio.BufferProcessor; +import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.vr.sdk.audio.GvrAudioSurround; import java.nio.ByteBuffer; import java.nio.ByteOrder; /** - * Buffer processor that uses {@code GvrAudioSurround} to provide binaural rendering of surround - * sound and ambisonic soundfields. + * An {@link AudioProcessor} that uses {@code GvrAudioSurround} to provide binaural rendering of + * surround sound and ambisonic soundfields. */ -public final class GvrBufferProcessor implements BufferProcessor { +public final class GvrAudioProcessor implements AudioProcessor { private static final int FRAMES_PER_OUTPUT_BUFFER = 1024; private static final int OUTPUT_CHANNEL_COUNT = 2; @@ -43,7 +43,7 @@ public final class GvrBufferProcessor implements BufferProcessor { private float y; private float z; - public GvrBufferProcessor() { + public GvrAudioProcessor() { // Use the identity for the initial orientation. w = 1f; sampleRateHz = Format.NO_VALUE; diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index f31f80f518..564a41fc77 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.ext.opus; import android.os.Handler; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; -import com.google.android.exoplayer2.audio.BufferProcessor; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; @@ -40,26 +40,24 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers - * before they are output. + * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - BufferProcessor... bufferProcessors) { - super(eventHandler, eventListener, bufferProcessors); + AudioProcessor... audioProcessors) { + super(eventHandler, eventListener, audioProcessors); } /** * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio - * buffers before they are output. + * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, - BufferProcessor... bufferProcessors) { + AudioProcessor... audioProcessors) { super(eventHandler, eventListener, null, drmSessionManager, playClearSamplesWithoutKeys, - bufferProcessors); + audioProcessors); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 4547ec7e08..3ce4937911 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -28,8 +28,8 @@ import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; import com.google.android.exoplayer2.audio.AudioCapabilities; +import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; -import com.google.android.exoplayer2.audio.BufferProcessor; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -625,7 +625,7 @@ public class SimpleExoPlayer implements ExoPlayer { buildVideoRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, componentListener, allowedVideoJoiningTimeMs, out); buildAudioRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, - componentListener, buildBufferProcessors(), out); + componentListener, buildAudioProcessors(), out); buildTextRenderers(context, mainHandler, extensionRendererMode, componentListener, out); buildMetadataRenderers(context, mainHandler, extensionRendererMode, componentListener, out); buildMiscellaneousRenderers(context, mainHandler, extensionRendererMode, out); @@ -685,16 +685,16 @@ public class SimpleExoPlayer implements ExoPlayer { * not be used for DRM protected playbacks. * @param extensionRendererMode The extension renderer mode. * @param eventListener An event listener. - * @param bufferProcessors An array of {@link BufferProcessor}s which will process PCM audio - * buffers before they are output. May be empty. + * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio buffers + * before output. May be empty. * @param out An array to which the built renderers should be appended. */ protected void buildAudioRenderers(Context context, Handler mainHandler, DrmSessionManager drmSessionManager, @ExtensionRendererMode int extensionRendererMode, AudioRendererEventListener eventListener, - BufferProcessor[] bufferProcessors, ArrayList out) { + AudioProcessor[] audioProcessors, ArrayList out) { out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true, - mainHandler, eventListener, AudioCapabilities.getCapabilities(context), bufferProcessors)); + mainHandler, eventListener, AudioCapabilities.getCapabilities(context), audioProcessors)); if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { return; @@ -708,9 +708,9 @@ public class SimpleExoPlayer implements ExoPlayer { Class clazz = Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class, BufferProcessor[].class); + AudioRendererEventListener.class, AudioProcessor[].class); Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener, - bufferProcessors); + audioProcessors); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded LibopusAudioRenderer."); } catch (ClassNotFoundException e) { @@ -723,9 +723,9 @@ public class SimpleExoPlayer implements ExoPlayer { Class clazz = Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class, BufferProcessor[].class); + AudioRendererEventListener.class, AudioProcessor[].class); Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener, - bufferProcessors); + audioProcessors); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded LibflacAudioRenderer."); } catch (ClassNotFoundException e) { @@ -738,9 +738,9 @@ public class SimpleExoPlayer implements ExoPlayer { Class clazz = Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class, BufferProcessor[].class); + AudioRendererEventListener.class, AudioProcessor[].class); Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener, - bufferProcessors); + audioProcessors); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded FfmpegAudioRenderer."); } catch (ClassNotFoundException e) { @@ -794,11 +794,10 @@ public class SimpleExoPlayer implements ExoPlayer { } /** - * Builds an array of {@link BufferProcessor}s which will process PCM audio buffers before they - * are output. + * Builds an array of {@link AudioProcessor}s that will process PCM audio before output. */ - protected BufferProcessor[] buildBufferProcessors() { - return new BufferProcessor[0]; + protected AudioProcessor[] buildAudioProcessors() { + return new AudioProcessor[0]; } // Internal methods. diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java similarity index 95% rename from library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java rename to library/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java index 87d4e5fe7b..2e0d1f98d9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java @@ -20,12 +20,12 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; /** - * Interface for processors of audio buffers. + * Interface for audio processors. */ -public interface BufferProcessor { +public interface AudioProcessor { /** - * Exception thrown when a processor can't be configured for a given input format. + * Exception thrown when a processor can't be configured for a given input audio format. */ final class UnhandledFormatException extends Exception { @@ -42,7 +42,7 @@ public interface BufferProcessor { ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder()); /** - * Configures the processor to process input buffers with the specified format. After calling this + * Configures the processor to process input audio with the specified format. After calling this * method, {@link #isActive()} returns whether the processor needs to handle buffers; if not, the * processor will not accept any buffers until it is reconfigured. Returns {@code true} if the * processor must be flushed, or if the value returned by {@link #isActive()} has changed as a @@ -111,7 +111,7 @@ public interface BufferProcessor { boolean isEnded(); /** - * Clears any state in preparation for receiving a new stream of buffers. + * Clears any state in preparation for receiving a new stream of input buffers. */ void flush(); diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index 76b5ec72fe..3b8a1b8f39 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -270,8 +270,8 @@ public final class AudioTrack { public static boolean failOnSpuriousAudioTimestamp = false; private final AudioCapabilities audioCapabilities; - private final ChannelMappingBufferProcessor channelMappingBufferProcessor; - private final BufferProcessor[] availableBufferProcessors; + private final ChannelMappingAudioProcessor channelMappingAudioProcessor; + private final AudioProcessor[] availableAudioProcessors; private final Listener listener; private final ConditionVariable releasingConditionVariable; private final long[] playheadOffsets; @@ -319,13 +319,13 @@ public final class AudioTrack { private long latencyUs; private float volume; - private BufferProcessor[] bufferProcessors; + private AudioProcessor[] audioProcessors; private ByteBuffer[] outputBuffers; private ByteBuffer inputBuffer; private ByteBuffer outputBuffer; private byte[] preV21OutputBuffer; private int preV21OutputBufferOffset; - private int drainingBufferProcessorIndex; + private int drainingAudioProcessorIndex; private boolean handledEndOfStream; private boolean playing; @@ -337,18 +337,18 @@ public final class AudioTrack { /** * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. - * @param bufferProcessors An array of {@link BufferProcessor}s which will process PCM audio - * buffers before they are output. May be empty. + * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before + * output. May be empty. * @param listener Listener for audio track events. */ - public AudioTrack(AudioCapabilities audioCapabilities, BufferProcessor[] bufferProcessors, + public AudioTrack(AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors, Listener listener) { this.audioCapabilities = audioCapabilities; - channelMappingBufferProcessor = new ChannelMappingBufferProcessor(); - availableBufferProcessors = new BufferProcessor[bufferProcessors.length + 2]; - availableBufferProcessors[0] = new ResamplingBufferProcessor(); - availableBufferProcessors[1] = channelMappingBufferProcessor; - System.arraycopy(bufferProcessors, 0, availableBufferProcessors, 2, bufferProcessors.length); + channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); + availableAudioProcessors = new AudioProcessor[audioProcessors.length + 2]; + availableAudioProcessors[0] = new ResamplingAudioProcessor(); + availableAudioProcessors[1] = channelMappingAudioProcessor; + System.arraycopy(audioProcessors, 0, availableAudioProcessors, 2, audioProcessors.length); this.listener = listener; releasingConditionVariable = new ConditionVariable(true); if (Util.SDK_INT >= 18) { @@ -371,8 +371,8 @@ public final class AudioTrack { startMediaTimeState = START_NOT_SET; streamType = C.STREAM_TYPE_DEFAULT; audioSessionId = C.AUDIO_SESSION_ID_UNSET; - drainingBufferProcessorIndex = C.INDEX_UNSET; - this.bufferProcessors = new BufferProcessor[0]; + drainingAudioProcessorIndex = C.INDEX_UNSET; + this.audioProcessors = new AudioProcessor[0]; outputBuffers = new ByteBuffer[0]; } @@ -482,32 +482,32 @@ public final class AudioTrack { if (!passthrough) { pcmFrameSize = Util.getPcmFrameSize(pcmEncoding, channelCount); - // Reconfigure the buffer processors. - channelMappingBufferProcessor.setChannelMap(outputChannels); - ArrayList newBufferProcessors = new ArrayList<>(); - for (BufferProcessor bufferProcessor : availableBufferProcessors) { + // Reconfigure the audio processors. + channelMappingAudioProcessor.setChannelMap(outputChannels); + ArrayList newAudioProcessors = new ArrayList<>(); + for (AudioProcessor audioProcessor : availableAudioProcessors) { try { - flush |= bufferProcessor.configure(sampleRate, channelCount, encoding); - } catch (BufferProcessor.UnhandledFormatException e) { + flush |= audioProcessor.configure(sampleRate, channelCount, encoding); + } catch (AudioProcessor.UnhandledFormatException e) { throw new ConfigurationException(e); } - if (bufferProcessor.isActive()) { - newBufferProcessors.add(bufferProcessor); - channelCount = bufferProcessor.getOutputChannelCount(); - encoding = bufferProcessor.getOutputEncoding(); + if (audioProcessor.isActive()) { + newAudioProcessors.add(audioProcessor); + channelCount = audioProcessor.getOutputChannelCount(); + encoding = audioProcessor.getOutputEncoding(); } else { - bufferProcessor.flush(); + audioProcessor.flush(); } } if (flush) { - int count = newBufferProcessors.size(); - bufferProcessors = newBufferProcessors.toArray(new BufferProcessor[count]); + int count = newAudioProcessors.size(); + audioProcessors = newAudioProcessors.toArray(new AudioProcessor[count]); outputBuffers = new ByteBuffer[count]; for (int i = 0; i < count; i++) { - BufferProcessor bufferProcessor = bufferProcessors[i]; - bufferProcessor.flush(); - outputBuffers[i] = bufferProcessor.getOutput(); + AudioProcessor audioProcessor = audioProcessors[i]; + audioProcessor.flush(); + outputBuffers[i] = audioProcessor.getOutput(); } } } @@ -787,20 +787,20 @@ public final class AudioTrack { } private void processBuffers(long avSyncPresentationTimeUs) throws WriteException { - int count = bufferProcessors.length; + int count = audioProcessors.length; int index = count; while (index >= 0) { ByteBuffer input = index > 0 ? outputBuffers[index - 1] - : (inputBuffer != null ? inputBuffer : BufferProcessor.EMPTY_BUFFER); + : (inputBuffer != null ? inputBuffer : AudioProcessor.EMPTY_BUFFER); if (index == count) { writeBuffer(input, avSyncPresentationTimeUs); } else { - BufferProcessor bufferProcessor = bufferProcessors[index]; - bufferProcessor.queueInput(input); - ByteBuffer output = bufferProcessor.getOutput(); + AudioProcessor audioProcessor = audioProcessors[index]; + audioProcessor.queueInput(input); + ByteBuffer output = audioProcessor.getOutput(); outputBuffers[index] = output; if (output.hasRemaining()) { - // Handle the output as input to the next buffer processor or the AudioTrack. + // Handle the output as input to the next audio processor or the AudioTrack. index++; continue; } @@ -889,23 +889,23 @@ public final class AudioTrack { return; } - // Drain the buffer processors. - boolean bufferProcessorNeedsEndOfStream = false; - if (drainingBufferProcessorIndex == C.INDEX_UNSET) { - drainingBufferProcessorIndex = passthrough ? bufferProcessors.length : 0; - bufferProcessorNeedsEndOfStream = true; + // Drain the audio processors. + boolean audioProcessorNeedsEndOfStream = false; + if (drainingAudioProcessorIndex == C.INDEX_UNSET) { + drainingAudioProcessorIndex = passthrough ? audioProcessors.length : 0; + audioProcessorNeedsEndOfStream = true; } - while (drainingBufferProcessorIndex < bufferProcessors.length) { - BufferProcessor bufferProcessor = bufferProcessors[drainingBufferProcessorIndex]; - if (bufferProcessorNeedsEndOfStream) { - bufferProcessor.queueEndOfStream(); + while (drainingAudioProcessorIndex < audioProcessors.length) { + AudioProcessor audioProcessor = audioProcessors[drainingAudioProcessorIndex]; + if (audioProcessorNeedsEndOfStream) { + audioProcessor.queueEndOfStream(); } processBuffers(C.TIME_UNSET); - if (!bufferProcessor.isEnded()) { + if (!audioProcessor.isEnded()) { return; } - bufferProcessorNeedsEndOfStream = true; - drainingBufferProcessorIndex++; + audioProcessorNeedsEndOfStream = true; + drainingAudioProcessorIndex++; } // Finish writing any remaining output to the track. @@ -989,8 +989,8 @@ public final class AudioTrack { * Enables tunneling. The audio track is reset if tunneling was previously disabled or if the * audio session id has changed. Enabling tunneling requires platform API version 21 onwards. *

      - * If this instance has {@link BufferProcessor}s and tunneling is enabled, care must be taken that - * buffer processors do not output buffers with a different duration than their input, and buffer + * If this instance has {@link AudioProcessor}s and tunneling is enabled, care must be taken that + * audio processors do not output buffers with a different duration than their input, and buffer * processors must produce output corresponding to their last input immediately after that input * is queued. * @@ -1067,13 +1067,13 @@ public final class AudioTrack { framesPerEncodedSample = 0; inputBuffer = null; outputBuffer = null; - for (int i = 0; i < bufferProcessors.length; i++) { - BufferProcessor bufferProcessor = bufferProcessors[i]; - bufferProcessor.flush(); - outputBuffers[i] = bufferProcessor.getOutput(); + for (int i = 0; i < audioProcessors.length; i++) { + AudioProcessor audioProcessor = audioProcessors[i]; + audioProcessor.flush(); + outputBuffers[i] = audioProcessor.getOutput(); } handledEndOfStream = false; - drainingBufferProcessorIndex = C.INDEX_UNSET; + drainingAudioProcessorIndex = C.INDEX_UNSET; avSyncHeader = null; bytesUntilNextAvSync = 0; startMediaTimeState = START_NOT_SET; @@ -1108,8 +1108,8 @@ public final class AudioTrack { public void release() { reset(); releaseKeepSessionIdAudioTrack(); - for (BufferProcessor bufferProcessor : availableBufferProcessors) { - bufferProcessor.release(); + for (AudioProcessor audioProcessor : availableAudioProcessors) { + audioProcessor.release(); } audioSessionId = C.AUDIO_SESSION_ID_UNSET; playing = false; diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingBufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java similarity index 93% rename from library/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingBufferProcessor.java rename to library/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java index 8c23198925..e81d7e218a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingBufferProcessor.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -22,10 +22,10 @@ import java.nio.ByteOrder; import java.util.Arrays; /** - * Buffer processor that applies a mapping from input channels onto specified output channels. This - * can be used to reorder, duplicate or discard channels. + * An {@link AudioProcessor} that applies a mapping from input channels onto specified output + * channels. This can be used to reorder, duplicate or discard channels. */ -/* package */ final class ChannelMappingBufferProcessor implements BufferProcessor { +/* package */ final class ChannelMappingAudioProcessor implements AudioProcessor { private int channelCount; private int sampleRateHz; @@ -40,7 +40,7 @@ import java.util.Arrays; /** * Creates a new processor that applies a channel mapping. */ - public ChannelMappingBufferProcessor() { + public ChannelMappingAudioProcessor() { buffer = EMPTY_BUFFER; outputBuffer = EMPTY_BUFFER; } diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 76f7ac08bb..e34068861d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -123,16 +123,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @param eventListener A listener of events. May be null if delivery of events is not required. * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. - * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers - * before they are output. + * @param audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before + * output. */ public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, Handler eventHandler, AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, - BufferProcessor... bufferProcessors) { + AudioProcessor... audioProcessors) { super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); - audioTrack = new AudioTrack(audioCapabilities, bufferProcessors, new AudioTrackListener()); + audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener()); eventDispatcher = new EventDispatcher(eventHandler, eventListener); } diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java similarity index 94% rename from library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java rename to library/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java index 370e54c58d..752f55a0ca 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java @@ -21,9 +21,9 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; /** - * A {@link BufferProcessor} that converts audio data to {@link C#ENCODING_PCM_16BIT}. + * An {@link AudioProcessor} that converts audio data to {@link C#ENCODING_PCM_16BIT}. */ -/* package */ final class ResamplingBufferProcessor implements BufferProcessor { +/* package */ final class ResamplingAudioProcessor implements AudioProcessor { private int sampleRateHz; private int channelCount; @@ -34,9 +34,9 @@ import java.nio.ByteOrder; private boolean inputEnded; /** - * Creates a new buffer processor that converts audio data to {@link C#ENCODING_PCM_16BIT}. + * Creates a new audio processor that converts audio data to {@link C#ENCODING_PCM_16BIT}. */ - public ResamplingBufferProcessor() { + public ResamplingAudioProcessor() { sampleRateHz = Format.NO_VALUE; channelCount = Format.NO_VALUE; encoding = C.ENCODING_INVALID; diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index e80c9bb70a..5594d9a90e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -102,12 +102,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers - * before they are output. + * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ public SimpleDecoderAudioRenderer(Handler eventHandler, - AudioRendererEventListener eventListener, BufferProcessor... bufferProcessors) { - this(eventHandler, eventListener, null, null, false, bufferProcessors); + AudioRendererEventListener eventListener, AudioProcessor... audioProcessors) { + this(eventHandler, eventListener, null, null, false, audioProcessors); } /** @@ -135,18 +134,17 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * begin in parallel with key acquisition. This parameter specifies whether the renderer is * permitted to play clear regions of encrypted media files before {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. - * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio - * buffers before they are output. + * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, - BufferProcessor... bufferProcessors) { + AudioProcessor... audioProcessors) { super(C.TRACK_TYPE_AUDIO); this.drmSessionManager = drmSessionManager; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; eventDispatcher = new EventDispatcher(eventHandler, eventListener); - audioTrack = new AudioTrack(audioCapabilities, bufferProcessors, new AudioTrackListener()); + audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener()); formatHolder = new FormatHolder(); flagsOnlyBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); decoderReinitializationState = REINITIALIZATION_STATE_NONE; From ab8fd14724b0ee38e74b53cc5c9fed1c38c512b6 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 28 Feb 2017 06:54:27 -0800 Subject: [PATCH 092/140] Support multiple track outputs from BaseMediaChunk Issue: #2362 Issue: #2176 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148764237 --- .../exoplayer2/drm/OfflineLicenseHelper.java | 3 +- .../source/chunk/BaseMediaChunk.java | 32 ++-- .../source/chunk/BaseMediaChunkOutput.java | 79 ++++++++++ .../source/chunk/ChunkExtractorWrapper.java | 140 ++++++++++++------ .../source/chunk/ChunkSampleStream.java | 12 +- .../source/chunk/ContainerMediaChunk.java | 9 +- .../source/chunk/SingleSampleMediaChunk.java | 15 +- .../exoplayer2/source/dash/DashUtil.java | 28 ++-- .../source/dash/DefaultDashChunkSource.java | 9 +- .../smoothstreaming/DefaultSsChunkSource.java | 2 +- 10 files changed, 229 insertions(+), 100 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 6a7f905a51..ad44574af9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -177,8 +177,7 @@ public final class OfflineLicenseHelper { Representation representation = adaptationSet.representations.get(0); DrmInitData drmInitData = representation.format.drmInitData; if (drmInitData == null) { - Format sampleFormat = DashUtil.loadSampleFormat(dataSource, representation, - adaptationSet.type); + Format sampleFormat = DashUtil.loadSampleFormat(dataSource, representation); if (sampleFormat != null) { drmInitData = sampleFormat.drmInitData; } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java index 0a43ecde63..7a5aeabeb6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java @@ -21,14 +21,12 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; /** - * A base implementation of {@link MediaChunk}, for chunks that contain a single track. - *

      - * Loaded samples are output to a {@link DefaultTrackOutput}. + * A base implementation of {@link MediaChunk} that outputs to a {@link BaseMediaChunkOutput}. */ public abstract class BaseMediaChunk extends MediaChunk { - private DefaultTrackOutput trackOutput; - private int firstSampleIndex; + private BaseMediaChunkOutput output; + private int[] firstSampleIndices; /** * @param dataSource The source from which the data should be loaded. @@ -48,29 +46,29 @@ public abstract class BaseMediaChunk extends MediaChunk { } /** - * Initializes the chunk for loading, setting the {@link DefaultTrackOutput} that will receive + * Initializes the chunk for loading, setting the {@link BaseMediaChunkOutput} that will receive * samples as they are loaded. * - * @param trackOutput The output that will receive the loaded samples. + * @param output The output that will receive the loaded media samples. */ - public void init(DefaultTrackOutput trackOutput) { - this.trackOutput = trackOutput; - this.firstSampleIndex = trackOutput.getWriteIndex(); + public void init(BaseMediaChunkOutput output) { + this.output = output; + firstSampleIndices = output.getWriteIndices(); } /** - * Returns the index of the first sample in the output that was passed to - * {@link #init(DefaultTrackOutput)} that will originate from this chunk. + * Returns the index of the first sample in the specified track of the output that will originate + * from this chunk. */ - public final int getFirstSampleIndex() { - return firstSampleIndex; + public final int getFirstSampleIndex(int trackIndex) { + return firstSampleIndices[trackIndex]; } /** - * Returns the track output most recently passed to {@link #init(DefaultTrackOutput)}. + * Returns the output most recently passed to {@link #init(BaseMediaChunkOutput)}. */ - protected final DefaultTrackOutput getTrackOutput() { - return trackOutput; + protected final BaseMediaChunkOutput getOutput() { + return output; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java new file mode 100644 index 0000000000..a429a7cab9 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2017 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.source.chunk; + +import android.util.Log; +import com.google.android.exoplayer2.extractor.DefaultTrackOutput; +import com.google.android.exoplayer2.extractor.DummyTrackOutput; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider; + +/** + * An output for {@link BaseMediaChunk}s. + */ +/* package */ final class BaseMediaChunkOutput implements TrackOutputProvider { + + private static final String TAG = "BaseMediaChunkOutput"; + + private final int[] trackTypes; + private final DefaultTrackOutput[] trackOutputs; + + /** + * @param trackTypes The track types of the individual track outputs. + * @param trackOutputs The individual track outputs. + */ + public BaseMediaChunkOutput(int[] trackTypes, DefaultTrackOutput... trackOutputs) { + this.trackTypes = trackTypes; + this.trackOutputs = trackOutputs; + } + + @Override + public TrackOutput track(int id, int type) { + for (int i = 0; i < trackTypes.length; i++) { + if (type == trackTypes[i]) { + return trackOutputs[i]; + } + } + Log.e(TAG, "Unmatched track of type: " + type); + return new DummyTrackOutput(); + } + + /** + * Returns the current absolute write indices of the individual track outputs. + */ + public int[] getWriteIndices() { + int[] writeIndices = new int[trackOutputs.length]; + for (int i = 0; i < trackOutputs.length; i++) { + if (trackOutputs[i] != null) { + writeIndices[i] = trackOutputs[i].getWriteIndex(); + } + } + return writeIndices; + } + + /** + * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples + * subsequently written to the track outputs. + */ + public void setSampleOffsetUs(long sampleOffsetUs) { + for (DefaultTrackOutput trackOutput : trackOutputs) { + if (trackOutput != null) { + trackOutput.setSampleOffsetUs(sampleOffsetUs); + } + } + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java index 2a641b80a6..501f4998cf 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.DummyTrackOutput; @@ -32,33 +33,46 @@ import java.io.IOException; *

      * The wrapper allows switching of the {@link TrackOutput} that receives parsed data. */ -public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput { +public final class ChunkExtractorWrapper implements ExtractorOutput { + + /** + * Provides {@link TrackOutput} instances to be written to by the wrapper. + */ + public interface TrackOutputProvider { + + /** + * Called to get the {@link TrackOutput} for a specific track. + *

      + * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}. + * + * @param id A track identifier. + * @param type The type of the track. Typically one of the + * {@link com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants. + * @return The {@link TrackOutput} for the given track identifier. + */ + TrackOutput track(int id, int type); + + } public final Extractor extractor; private final Format manifestFormat; - private final int primaryTrackType; + private final SparseArray bindingTrackOutputs; private boolean extractorInitialized; - private TrackOutput trackOutput; + private TrackOutputProvider trackOutputProvider; private SeekMap seekMap; - private Format sampleFormat; - - // Accessed only on the loader thread. - private boolean seenTrack; - private int seenTrackId; + private Format[] sampleFormats; /** * @param extractor The extractor to wrap. * @param manifestFormat A manifest defined {@link Format} whose data should be merged into any * sample {@link Format} output from the {@link Extractor}. - * @param primaryTrackType The type of the primary track. Typically one of the {@link C} - * {@code TRACK_TYPE_*} constants. */ - public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat, int primaryTrackType) { + public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat) { this.extractor = extractor; this.manifestFormat = manifestFormat; - this.primaryTrackType = primaryTrackType; + bindingTrackOutputs = new SparseArray<>(); } /** @@ -69,27 +83,27 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput } /** - * Returns the sample {@link Format} most recently output by the extractor, or null. + * Returns the sample {@link Format}s most recently output by the extractor, or null. */ - public Format getSampleFormat() { - return sampleFormat; + public Format[] getSampleFormats() { + return sampleFormats; } /** * Initializes the extractor to output to the provided {@link TrackOutput}, and configures it to * receive data from a new chunk. * - * @param trackOutput The {@link TrackOutput} that will receive sample data. + * @param trackOutputProvider The provider of {@link TrackOutput}s that will receive sample data. */ - public void init(TrackOutput trackOutput) { - this.trackOutput = trackOutput; + public void init(TrackOutputProvider trackOutputProvider) { + this.trackOutputProvider = trackOutputProvider; if (!extractorInitialized) { extractor.init(this); extractorInitialized = true; } else { extractor.seek(0, 0); - if (sampleFormat != null && trackOutput != null) { - trackOutput.format(sampleFormat); + for (int i = 0; i < bindingTrackOutputs.size(); i++) { + bindingTrackOutputs.valueAt(i).bind(trackOutputProvider); } } } @@ -98,18 +112,24 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput @Override public TrackOutput track(int id, int type) { - if (primaryTrackType != C.TRACK_TYPE_UNKNOWN && primaryTrackType != type) { - return new DummyTrackOutput(); + BindingTrackOutput bindingTrackOutput = bindingTrackOutputs.get(id); + if (bindingTrackOutput == null) { + // Assert that if we're seeing a new track we have not seen endTracks. + Assertions.checkState(sampleFormats == null); + bindingTrackOutput = new BindingTrackOutput(id, type, manifestFormat); + bindingTrackOutput.bind(trackOutputProvider); + bindingTrackOutputs.put(id, bindingTrackOutput); } - Assertions.checkState(!seenTrack || seenTrackId == id); - seenTrack = true; - seenTrackId = id; - return this; + return bindingTrackOutput; } @Override public void endTracks() { - Assertions.checkState(seenTrack); + Format[] sampleFormats = new Format[bindingTrackOutputs.size()]; + for (int i = 0; i < bindingTrackOutputs.size(); i++) { + sampleFormats[i] = bindingTrackOutputs.valueAt(i).sampleFormat; + } + this.sampleFormats = sampleFormats; } @Override @@ -117,31 +137,59 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput this.seekMap = seekMap; } - // TrackOutput implementation. + // Internal logic. - @Override - public void format(Format format) { - sampleFormat = format.copyWithManifestFormatInfo(manifestFormat); - if (trackOutput != null) { + private static final class BindingTrackOutput implements TrackOutput { + + private final int id; + private final int type; + private final Format manifestFormat; + + public Format sampleFormat; + private TrackOutput trackOutput; + + public BindingTrackOutput(int id, int type, Format manifestFormat) { + this.id = id; + this.type = type; + this.manifestFormat = manifestFormat; + } + + public void bind(TrackOutputProvider trackOutputProvider) { + if (trackOutputProvider == null) { + trackOutput = new DummyTrackOutput(); + return; + } + trackOutput = trackOutputProvider.track(id, type); + if (trackOutput != null) { + trackOutput.format(sampleFormat); + } + } + + @Override + public void format(Format format) { + // TODO: This should only happen for the primary track. Additional metadata/text tracks need + // to be copied with different manifest derived formats. + sampleFormat = format.copyWithManifestFormatInfo(manifestFormat); trackOutput.format(sampleFormat); } - } - @Override - public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) - throws IOException, InterruptedException { - return trackOutput.sampleData(input, length, allowEndOfInput); - } + @Override + public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + return trackOutput.sampleData(input, length, allowEndOfInput); + } - @Override - public void sampleData(ParsableByteArray data, int length) { - trackOutput.sampleData(data, length); - } + @Override + public void sampleData(ParsableByteArray data, int length) { + trackOutput.sampleData(data, length); + } + + @Override + public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, + byte[] encryptionKey) { + trackOutput.sampleMetadata(timeUs, flags, size, offset, encryptionKey); + } - @Override - public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey) { - trackOutput.sampleMetadata(timeUs, flags, size, offset, encryptionKey); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 7149ce3f99..909bf317b3 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -44,6 +44,7 @@ public class ChunkSampleStream implements SampleStream, S private final int minLoadableRetryCount; private final LinkedList mediaChunks; private final List readOnlyMediaChunks; + private final BaseMediaChunkOutput mediaChunkOutput; private final DefaultTrackOutput sampleQueue; private final ChunkHolder nextChunkHolder; private final Loader loader; @@ -78,6 +79,7 @@ public class ChunkSampleStream implements SampleStream, S mediaChunks = new LinkedList<>(); readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); sampleQueue = new DefaultTrackOutput(allocator); + mediaChunkOutput = new BaseMediaChunkOutput(new int[] {trackType}, sampleQueue); lastSeekPositionUs = positionUs; pendingResetPositionUs = positionUs; } @@ -127,7 +129,7 @@ public class ChunkSampleStream implements SampleStream, S if (seekInsideBuffer) { // We succeeded. All we need to do is discard any chunks that we've moved past. while (mediaChunks.size() > 1 - && mediaChunks.get(1).getFirstSampleIndex() <= sampleQueue.getReadIndex()) { + && mediaChunks.get(1).getFirstSampleIndex(0) <= sampleQueue.getReadIndex()) { mediaChunks.removeFirst(); } } else { @@ -176,7 +178,7 @@ public class ChunkSampleStream implements SampleStream, S } while (mediaChunks.size() > 1 - && mediaChunks.get(1).getFirstSampleIndex() <= sampleQueue.getReadIndex()) { + && mediaChunks.get(1).getFirstSampleIndex(0) <= sampleQueue.getReadIndex()) { mediaChunks.removeFirst(); } BaseMediaChunk currentChunk = mediaChunks.getFirst(); @@ -232,7 +234,7 @@ public class ChunkSampleStream implements SampleStream, S if (isMediaChunk) { BaseMediaChunk removed = mediaChunks.removeLast(); Assertions.checkState(removed == loadable); - sampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex()); + sampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); if (mediaChunks.isEmpty()) { pendingResetPositionUs = lastSeekPositionUs; } @@ -277,7 +279,7 @@ public class ChunkSampleStream implements SampleStream, S if (isMediaChunk(loadable)) { pendingResetPositionUs = C.TIME_UNSET; BaseMediaChunk mediaChunk = (BaseMediaChunk) loadable; - mediaChunk.init(sampleQueue); + mediaChunk.init(mediaChunkOutput); mediaChunks.add(mediaChunk); } long elapsedRealtimeMs = loader.startLoading(loadable, this, minLoadableRetryCount); @@ -337,7 +339,7 @@ public class ChunkSampleStream implements SampleStream, S startTimeUs = removed.startTimeUs; loadingFinished = false; } - sampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex()); + sampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); eventDispatcher.upstreamDiscarded(trackType, startTimeUs, endTimeUs); return true; } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java index 44fd45d5ff..cfbefc0c2e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source.chunk; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; -import com.google.android.exoplayer2.extractor.DefaultTrackOutput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.upstream.DataSource; @@ -100,10 +99,10 @@ public class ContainerMediaChunk extends BaseMediaChunk { ExtractorInput input = new DefaultExtractorInput(dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); if (bytesLoaded == 0) { - // Set the target to ourselves. - DefaultTrackOutput trackOutput = getTrackOutput(); - trackOutput.setSampleOffsetUs(sampleOffsetUs); - extractorWrapper.init(trackOutput); + // Configure the output and set it as the target for the extractor wrapper. + BaseMediaChunkOutput output = getOutput(); + output.setSampleOffsetUs(sampleOffsetUs); + extractorWrapper.init(output); } // Load and decode the sample data. try { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java index 1afce6f2ee..a008c9cd84 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java @@ -18,8 +18,8 @@ package com.google.android.exoplayer2.source.chunk; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; -import com.google.android.exoplayer2.extractor.DefaultTrackOutput; import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Util; @@ -30,6 +30,7 @@ import java.io.IOException; */ public final class SingleSampleMediaChunk extends BaseMediaChunk { + private final int trackType; private final Format sampleFormat; private volatile int bytesLoaded; @@ -45,15 +46,20 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param chunkIndex The index of the chunk. + * @param trackType The type of the chunk. Typically one of the {@link C} {@code TRACK_TYPE_*} + * constants. + * @param sampleFormat The {@link Format} of the sample in the chunk. */ public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs, - int chunkIndex, Format sampleFormat) { + int chunkIndex, int trackType, Format sampleFormat) { super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); + this.trackType = trackType; this.sampleFormat = sampleFormat; } + @Override public boolean isLoadCompleted() { return loadCompleted; @@ -87,8 +93,9 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { length += bytesLoaded; } ExtractorInput extractorInput = new DefaultExtractorInput(dataSource, bytesLoaded, length); - DefaultTrackOutput trackOutput = getTrackOutput(); - trackOutput.setSampleOffsetUs(0); + BaseMediaChunkOutput output = getOutput(); + output.setSampleOffsetUs(0); + TrackOutput trackOutput = output.track(0, trackType); trackOutput.format(sampleFormat); // Load the sample data. int result = 0; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java index bc8d67816f..8fca21b2e0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java @@ -68,17 +68,15 @@ public final class DashUtil { * * @param dataSource The source from which the data should be loaded. * @param representation The representation which initialization chunk belongs to. - * @param type The type of the primary track. Typically one of the {@link C} {@code TRACK_TYPE_*} - * constants. * @return the sample {@link Format} of the given representation. * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - public static Format loadSampleFormat(DataSource dataSource, Representation representation, - int type) throws IOException, InterruptedException { + public static Format loadSampleFormat(DataSource dataSource, Representation representation) + throws IOException, InterruptedException { ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, representation, - type, false); - return extractorWrapper == null ? null : extractorWrapper.getSampleFormat(); + false); + return extractorWrapper == null ? null : extractorWrapper.getSampleFormats()[0]; } /** @@ -87,16 +85,14 @@ public final class DashUtil { * * @param dataSource The source from which the data should be loaded. * @param representation The representation which initialization chunk belongs to. - * @param type The type of the primary track. Typically one of the {@link C} {@code TRACK_TYPE_*} - * constants. * @return {@link ChunkIndex} of the given representation. * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - public static ChunkIndex loadChunkIndex(DataSource dataSource, Representation representation, - int type) throws IOException, InterruptedException { + public static ChunkIndex loadChunkIndex(DataSource dataSource, Representation representation) + throws IOException, InterruptedException { ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, representation, - type, true); + true); return extractorWrapper == null ? null : (ChunkIndex) extractorWrapper.getSeekMap(); } @@ -106,8 +102,6 @@ public final class DashUtil { * * @param dataSource The source from which the data should be loaded. * @param representation The representation which initialization chunk belongs to. - * @param type The type of the primary track. Typically one of the {@link C} {@code TRACK_TYPE_*} - * constants. * @param loadIndex Whether to load index data too. * @return A {@link ChunkExtractorWrapper} for the {@code representation}, or null if no * initialization or (if requested) index data exists. @@ -115,13 +109,13 @@ public final class DashUtil { * @throws InterruptedException Thrown if the thread was interrupted. */ private static ChunkExtractorWrapper loadInitializationData(DataSource dataSource, - Representation representation, int type, boolean loadIndex) + Representation representation, boolean loadIndex) throws IOException, InterruptedException { RangedUri initializationUri = representation.getInitializationUri(); if (initializationUri == null) { return null; } - ChunkExtractorWrapper extractorWrapper = newWrappedExtractor(representation.format, type); + ChunkExtractorWrapper extractorWrapper = newWrappedExtractor(representation.format); RangedUri requestUri; if (loadIndex) { RangedUri indexUri = representation.getIndexUri(); @@ -153,12 +147,12 @@ public final class DashUtil { initializationChunk.load(); } - private static ChunkExtractorWrapper newWrappedExtractor(Format format, int trackType) { + private static ChunkExtractorWrapper newWrappedExtractor(Format format) { String mimeType = format.containerMimeType; boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM); Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor(); - return new ChunkExtractorWrapper(extractor, format, trackType); + return new ChunkExtractorWrapper(extractor, format); } private DashUtil() {} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 7dd1294c22..7ccea8a2a6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -185,7 +185,7 @@ public class DefaultDashChunkSource implements DashChunkSource { RangedUri pendingInitializationUri = null; RangedUri pendingIndexUri = null; - if (representationHolder.extractorWrapper.getSampleFormat() == null) { + if (representationHolder.extractorWrapper.getSampleFormats() == null) { pendingInitializationUri = selectedRepresentation.getInitializationUri(); } if (segmentIndex == null) { @@ -343,7 +343,8 @@ public class DefaultDashChunkSource implements DashChunkSource { DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl), segmentUri.start, segmentUri.length, representation.getCacheKey()); return new SingleSampleMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, - trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, trackFormat); + trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, + representationHolder.trackType, trackFormat); } else { int segmentCount = 1; for (int i = 1; i < maxSegmentCount; i++) { @@ -370,6 +371,7 @@ public class DefaultDashChunkSource implements DashChunkSource { protected static final class RepresentationHolder { + public final int trackType; public final ChunkExtractorWrapper extractorWrapper; public Representation representation; @@ -382,6 +384,7 @@ public class DefaultDashChunkSource implements DashChunkSource { boolean enableEventMessageTrack, boolean enableCea608Track, int trackType) { this.periodDurationUs = periodDurationUs; this.representation = representation; + this.trackType = trackType; String containerMimeType = representation.format.containerMimeType; if (mimeTypeIsRawText(containerMimeType)) { extractorWrapper = null; @@ -403,7 +406,7 @@ public class DefaultDashChunkSource implements DashChunkSource { } // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. - extractorWrapper = new ChunkExtractorWrapper(extractor, representation.format, trackType); + extractorWrapper = new ChunkExtractorWrapper(extractor, representation.format); } segmentIndex = representation.getIndex(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index e17d72ab37..f2e4c57298 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -102,7 +102,7 @@ public class DefaultSsChunkSource implements SsChunkSource { FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, null, track); - extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format, streamElement.type); + extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format); } } From a9079f67aa031649edfa7427e0fa96bf65efa1ac Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 28 Feb 2017 09:09:01 -0800 Subject: [PATCH 093/140] Fix some documentation nits. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148776593 --- .../google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java | 3 +++ library/src/main/java/com/google/android/exoplayer2/C.java | 2 +- .../exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java index a53e1c97c5..2117985da0 100644 --- a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java +++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java @@ -43,6 +43,9 @@ public final class GvrAudioProcessor implements AudioProcessor { private float y; private float z; + /** + * Creates a new GVR audio processor. + */ public GvrAudioProcessor() { // Use the identity for the initial orientation. w = 1f; diff --git a/library/src/main/java/com/google/android/exoplayer2/C.java b/library/src/main/java/com/google/android/exoplayer2/C.java index ec7e6fa3de..6a1db191a0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/src/main/java/com/google/android/exoplayer2/C.java @@ -484,7 +484,7 @@ public final class C { * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object * should be a {@link android.media.PlaybackParams}, or null, which will be used to configure the * underlying {@link android.media.AudioTrack}. The message object should not be modified by the - * caller after it has been passed + * caller after it has been passed. */ public static final int MSG_SET_PLAYBACK_PARAMS = 3; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 587f036797..e8b664d5ab 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -33,7 +33,7 @@ import java.util.List; public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Factory { /** - * Flags controlling elementary stream readers behaviour. + * Flags controlling elementary stream readers' behavior. */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = {FLAG_ALLOW_NON_IDR_KEYFRAMES, FLAG_IGNORE_AAC_STREAM, From 5b0192a3ddfb819145396cafb94023322c363413 Mon Sep 17 00:00:00 2001 From: mofneko Date: Wed, 8 Mar 2017 03:20:34 +0900 Subject: [PATCH 094/140] Update HlsPlaylistTracker.java Fix unused variable. --- .../exoplayer2/source/hls/playlist/HlsPlaylistTracker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index 356aa0b466..e2e5870777 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -315,7 +315,7 @@ public final class HlsPlaylistTracker implements Loader.Callback Date: Tue, 28 Feb 2017 13:28:09 -0800 Subject: [PATCH 095/140] Deprecate instead of delete BaseRender.readSource(FormatHolder, DecoderInputBuffer) ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148808381 --- .../java/com/google/android/exoplayer2/BaseRenderer.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 7266e2cd30..f65be3afcd 100644 --- a/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -254,6 +254,14 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return index; } + /** + * Use {@link #readSource(FormatHolder, DecoderInputBuffer, boolean)} instead. + */ + @Deprecated + protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer) { + return readSource(formatHolder, buffer, false); + } + /** * Reads from the enabled upstream source. If the upstream source has been read to the end then * {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamFinal()} has been From 247da48e9dfd2366720bfdc58723e5d3dc8b3a98 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 2 Mar 2017 07:52:10 -0800 Subject: [PATCH 096/140] Make ElementaryStreamReader's public This allows building a TsPayloadReader.Factory without having to wrap the default one. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=149004102 --- .../com/google/android/exoplayer2/extractor/ts/Ac3Reader.java | 2 +- .../com/google/android/exoplayer2/extractor/ts/AdtsReader.java | 2 +- .../com/google/android/exoplayer2/extractor/ts/DtsReader.java | 2 +- .../com/google/android/exoplayer2/extractor/ts/H262Reader.java | 2 +- .../com/google/android/exoplayer2/extractor/ts/H264Reader.java | 2 +- .../com/google/android/exoplayer2/extractor/ts/H265Reader.java | 2 +- .../com/google/android/exoplayer2/extractor/ts/Id3Reader.java | 2 +- .../google/android/exoplayer2/extractor/ts/MpegAudioReader.java | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java index 790c036f1d..248161f28f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java @@ -27,7 +27,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; /** * Parses a continuous (E-)AC-3 byte stream and extracts individual samples. */ -/* package */ final class Ac3Reader implements ElementaryStreamReader { +public final class Ac3Reader implements ElementaryStreamReader { private static final int STATE_FINDING_SYNC = 0; private static final int STATE_READING_HEADER = 1; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java index 58318ea78d..7277df5bb8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java @@ -33,7 +33,7 @@ import java.util.Collections; /** * Parses a continuous ADTS byte stream and extracts individual frames. */ -/* package */ final class AdtsReader implements ElementaryStreamReader { +public final class AdtsReader implements ElementaryStreamReader { private static final String TAG = "AdtsReader"; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java index 874de83b68..df1e8816f0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; /** * Parses a continuous DTS byte stream and extracts individual samples. */ -/* package */ final class DtsReader implements ElementaryStreamReader { +public final class DtsReader implements ElementaryStreamReader { private static final int STATE_FINDING_SYNC = 0; private static final int STATE_READING_HEADER = 1; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index ba515d31ed..7266f847c4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -30,7 +30,7 @@ import java.util.Collections; /** * Parses a continuous H262 byte stream and extracts individual frames. */ -/* package */ final class H262Reader implements ElementaryStreamReader { +public final class H262Reader implements ElementaryStreamReader { private static final int START_PICTURE = 0x00; private static final int START_SEQUENCE_HEADER = 0xB3; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java index c1d24b7a33..8206ed7d6d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java @@ -33,7 +33,7 @@ import java.util.List; /** * Parses a continuous H264 byte stream and extracts individual frames. */ -/* package */ final class H264Reader implements ElementaryStreamReader { +public final class H264Reader implements ElementaryStreamReader { private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index 30a5bdc1fd..712ca8d69c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -30,7 +30,7 @@ import java.util.Collections; /** * Parses a continuous H.265 byte stream and extracts individual frames. */ -/* package */ final class H265Reader implements ElementaryStreamReader { +public final class H265Reader implements ElementaryStreamReader { private static final String TAG = "H265Reader"; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java index 7d2ecc4e74..98e1309143 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java @@ -27,7 +27,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; /** * Parses ID3 data and extracts individual text information frames. */ -/* package */ final class Id3Reader implements ElementaryStreamReader { +public final class Id3Reader implements ElementaryStreamReader { private static final String TAG = "Id3Reader"; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java index 6301716286..82fb84b291 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; /** * Parses a continuous MPEG Audio byte stream and extracts individual frames. */ -/* package */ final class MpegAudioReader implements ElementaryStreamReader { +public final class MpegAudioReader implements ElementaryStreamReader { private static final int STATE_FINDING_HEADER = 0; private static final int STATE_READING_HEADER = 1; From e40bba2852da6efb9e2141da0b237fe23900f393 Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 2 Mar 2017 08:12:01 -0800 Subject: [PATCH 097/140] Add Cache.getCachedBytes() which returns the length of the cached or not data block length This method can be used to determine not cached parts of a content. The 'length' parameter allows quicker responses without going through all adjacent spans. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=149005688 --- .../upstream/cache/SimpleCacheTest.java | 35 +++++++++ .../exoplayer2/upstream/cache/Cache.java | 12 +++ .../upstream/cache/CachedContent.java | 75 +++++++++---------- .../upstream/cache/SimpleCache.java | 8 +- 4 files changed, 89 insertions(+), 41 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index 001c6adc87..93d7a123bc 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -192,6 +192,41 @@ public class SimpleCacheTest extends InstrumentationTestCase { assertEquals(0, cacheDir.listFiles().length); } + + public void testGetCachedBytes() throws Exception { + SimpleCache simpleCache = getSimpleCache(); + CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0); + + // No cached bytes, returns -'length' + assertEquals(-100, simpleCache.getCachedBytes(KEY_1, 0, 100)); + + // Position value doesn't affect the return value + assertEquals(-100, simpleCache.getCachedBytes(KEY_1, 20, 100)); + + addCache(simpleCache, KEY_1, 0, 15); + + // Returns the length of a single span + assertEquals(15, simpleCache.getCachedBytes(KEY_1, 0, 100)); + + // Value is capped by the 'length' + assertEquals(10, simpleCache.getCachedBytes(KEY_1, 0, 10)); + + addCache(simpleCache, KEY_1, 15, 35); + + // Returns the length of two adjacent spans + assertEquals(50, simpleCache.getCachedBytes(KEY_1, 0, 100)); + + addCache(simpleCache, KEY_1, 60, 10); + + // Not adjacent span doesn't affect return value + assertEquals(50, simpleCache.getCachedBytes(KEY_1, 0, 100)); + + // Returns length of hole up to the next cached span + assertEquals(-5, simpleCache.getCachedBytes(KEY_1, 55, 100)); + + simpleCache.releaseHoleSpan(cacheSpan); + } + private SimpleCache getSimpleCache() { return new SimpleCache(cacheDir, new NoOpCacheEvictor()); } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java index 8dcfe75670..86ff810142 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java @@ -198,6 +198,18 @@ public interface Cache { */ boolean isCached(String key, long position, long length); + /** + * Returns the length of the cached data block starting from the {@code position} to the block end + * up to {@code length} bytes. If the {@code position} isn't cached then -(the length of the gap + * to the next cached data up to {@code length} bytes) is returned. + * + * @param key The cache key for the data. + * @param position The starting position of the data. + * @param length The maximum length of the data to be returned. + * @return the length of the cached or not cached data block length. + */ + long getCachedBytes(String key, long position, long length); + /** * Sets the content length for the given key. * diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java index c744a176ad..fb59d23666 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java @@ -106,43 +106,49 @@ import java.util.TreeSet; * which defines the maximum extents of the hole in the cache. */ public SimpleCacheSpan getSpan(long position) { - SimpleCacheSpan span = getSpanInternal(position); - if (!span.isCached) { - SimpleCacheSpan ceilEntry = cachedSpans.ceiling(span); - return ceilEntry == null ? SimpleCacheSpan.createOpenHole(key, position) - : SimpleCacheSpan.createClosedHole(key, position, ceilEntry.position - position); + SimpleCacheSpan lookupSpan = SimpleCacheSpan.createLookup(key, position); + SimpleCacheSpan floorSpan = cachedSpans.floor(lookupSpan); + if (floorSpan != null && floorSpan.position + floorSpan.length > position) { + return floorSpan; } - return span; + SimpleCacheSpan ceilSpan = cachedSpans.ceiling(lookupSpan); + return ceilSpan == null ? SimpleCacheSpan.createOpenHole(key, position) + : SimpleCacheSpan.createClosedHole(key, position, ceilSpan.position - position); } - /** Queries if a range is entirely available in the cache. */ - public boolean isCached(long position, long length) { - SimpleCacheSpan floorSpan = getSpanInternal(position); - if (!floorSpan.isCached) { + /** + * Returns the length of the cached data block starting from the {@code position} to the block end + * up to {@code length} bytes. If the {@code position} isn't cached then -(the length of the gap + * to the next cached data up to {@code length} bytes) is returned. + * + * @param position The starting position of the data. + * @param length The maximum length of the data to be returned. + * @return the length of the cached or not cached data block length. + */ + public long getCachedBytes(long position, long length) { + SimpleCacheSpan span = getSpan(position); + if (span.isHoleSpan()) { // We don't have a span covering the start of the queried region. - return false; + return -Math.min(span.isOpenEnded() ? Long.MAX_VALUE : span.length, length); } long queryEndPosition = position + length; - long currentEndPosition = floorSpan.position + floorSpan.length; - if (currentEndPosition >= queryEndPosition) { - // floorSpan covers the queried region. - return true; - } - for (SimpleCacheSpan next : cachedSpans.tailSet(floorSpan, false)) { - if (next.position > currentEndPosition) { - // There's a hole in the cache within the queried region. - return false; - } - // We expect currentEndPosition to always equal (next.position + next.length), but - // perform a max check anyway to guard against the existence of overlapping spans. - currentEndPosition = Math.max(currentEndPosition, next.position + next.length); - if (currentEndPosition >= queryEndPosition) { - // We've found spans covering the queried region. - return true; + long currentEndPosition = span.position + span.length; + if (currentEndPosition < queryEndPosition) { + for (SimpleCacheSpan next : cachedSpans.tailSet(span, false)) { + if (next.position > currentEndPosition) { + // There's a hole in the cache within the queried region. + break; + } + // We expect currentEndPosition to always equal (next.position + next.length), but + // perform a max check anyway to guard against the existence of overlapping spans. + currentEndPosition = Math.max(currentEndPosition, next.position + next.length); + if (currentEndPosition >= queryEndPosition) { + // We've found spans covering the queried region. + break; + } } } - // We ran out of spans before covering the queried region. - return false; + return Math.min(currentEndPosition - position, length); } /** @@ -190,15 +196,4 @@ import java.util.TreeSet; return result; } - /** - * Returns the span containing the position. If there isn't one, it returns the lookup span it - * used for searching. - */ - private SimpleCacheSpan getSpanInternal(long position) { - SimpleCacheSpan lookupSpan = SimpleCacheSpan.createLookup(key, position); - SimpleCacheSpan floorSpan = cachedSpans.floor(lookupSpan); - return floorSpan == null || floorSpan.position + floorSpan.length <= position ? lookupSpan - : floorSpan; - } - } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index e3e887c6ed..14f006c850 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -354,7 +354,13 @@ public final class SimpleCache implements Cache { @Override public synchronized boolean isCached(String key, long position, long length) { CachedContent cachedContent = index.get(key); - return cachedContent != null && cachedContent.isCached(position, length); + return cachedContent != null && cachedContent.getCachedBytes(position, length) >= length; + } + + @Override + public synchronized long getCachedBytes(String key, long position, long length) { + CachedContent cachedContent = index.get(key); + return cachedContent != null ? cachedContent.getCachedBytes(position, length) : -length; } @Override From 8e9711e8aacda21932cc9a9eabd424bab3cfb41b Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 2 Mar 2017 08:19:41 -0800 Subject: [PATCH 098/140] Allow packed audio without PRIV timestamps We use the segments' start time when the timestmap is not present. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=149006252 --- .../android/exoplayer2/source/hls/HlsMediaChunk.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 5fdc1f4e32..5615db1264 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.source.hls; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -250,10 +249,8 @@ import java.util.concurrent.atomic.AtomicInteger; if (extractor == null) { // Media segment format is packed audio. long id3Timestamp = peekId3PrivTimestamp(input); - if (id3Timestamp == C.TIME_UNSET) { - throw new ParserException("ID3 PRIV timestamp missing."); - } - extractor = buildPackedAudioExtractor(timestampAdjuster.adjustTsTimestamp(id3Timestamp)); + extractor = buildPackedAudioExtractor(id3Timestamp != C.TIME_UNSET + ? timestampAdjuster.adjustTsTimestamp(id3Timestamp) : startTimeUs); } if (skipLoadedBytes) { input.skipFully(bytesLoaded); From e7462f05f5b5d4fe71ac328ef526d08242d2154c Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 3 Mar 2017 03:24:42 -0800 Subject: [PATCH 099/140] Add maxVideoBitrate to DefaultTrackSelector.Parameters ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=149097876 --- .../trackselection/DefaultTrackSelector.java | 117 +++++++++++------- 1 file changed, 72 insertions(+), 45 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index f62d5d9075..f72f99f212 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -54,6 +54,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { public final boolean allowNonSeamlessAdaptiveness; public final int maxVideoWidth; public final int maxVideoHeight; + public final int maxVideoBitrate; public final boolean exceedVideoConstraintsIfNecessary; public final boolean exceedRendererCapabilitiesIfNecessary; public final int viewportWidth; @@ -68,14 +69,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { *

    1. Adaptation between different mime types is not allowed.
    2. *
    3. Non seamless adaptation is allowed.
    4. *
    5. No max limit for video width/height.
    6. + *
    7. No max video bitrate.
    8. *
    9. Video constraints are exceeded if no supported selection can be made otherwise.
    10. *
    11. Renderer capabilities are exceeded if no supported selection can be made.
    12. *
    13. No viewport width/height constraints are set.
    14. * */ public Parameters() { - this(null, null, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, true, true, - Integer.MAX_VALUE, Integer.MAX_VALUE, true); + this(null, null, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, true, + true, Integer.MAX_VALUE, Integer.MAX_VALUE, true); } /** @@ -88,6 +90,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param allowNonSeamlessAdaptiveness Whether non-seamless adaptation is allowed. * @param maxVideoWidth Maximum allowed video width. * @param maxVideoHeight Maximum allowed video height. + * @param maxVideoBitrate Maximum allowed video bitrate. * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no * selection can be made otherwise. * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no @@ -98,15 +101,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public Parameters(String preferredAudioLanguage, String preferredTextLanguage, boolean allowMixedMimeAdaptiveness, boolean allowNonSeamlessAdaptiveness, - int maxVideoWidth, int maxVideoHeight, boolean exceedVideoConstraintsIfNecessary, - boolean exceedRendererCapabilitiesIfNecessary, int viewportWidth, int viewportHeight, - boolean orientationMayChange) { + int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate, + boolean exceedVideoConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary, + int viewportWidth, int viewportHeight, boolean orientationMayChange) { this.preferredAudioLanguage = preferredAudioLanguage; this.preferredTextLanguage = preferredTextLanguage; this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness; this.allowNonSeamlessAdaptiveness = allowNonSeamlessAdaptiveness; this.maxVideoWidth = maxVideoWidth; this.maxVideoHeight = maxVideoHeight; + this.maxVideoBitrate = maxVideoBitrate; this.exceedVideoConstraintsIfNecessary = exceedVideoConstraintsIfNecessary; this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary; this.viewportWidth = viewportWidth; @@ -130,8 +134,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, orientationMayChange); + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -148,8 +152,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, orientationMayChange); + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -164,8 +168,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, orientationMayChange); + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -180,8 +184,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, orientationMayChange); + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -197,8 +201,24 @@ public class DefaultTrackSelector extends MappingTrackSelector { } return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, orientationMayChange); + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); + } + + /** + * Returns a {@link Parameters} instance with the provided max video bitrate. + * + * @param maxVideoBitrate The max video bitrate. + * @return A {@link Parameters} instance with the provided max video bitrate. + */ + public Parameters withMaxVideoBitrate(int maxVideoBitrate) { + if (maxVideoBitrate == this.maxVideoBitrate) { + return this; + } + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -235,8 +255,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, orientationMayChange); + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -255,8 +275,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, orientationMayChange); + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -275,8 +295,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, orientationMayChange); + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -319,6 +339,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { && exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary && orientationMayChange == other.orientationMayChange && viewportWidth == other.viewportWidth && viewportHeight == other.viewportHeight + && maxVideoBitrate == other.maxVideoBitrate && TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage) && TextUtils.equals(preferredTextLanguage, other.preferredTextLanguage); } @@ -331,6 +352,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { result = 31 * result + (allowNonSeamlessAdaptiveness ? 1 : 0); result = 31 * result + maxVideoWidth; result = 31 * result + maxVideoHeight; + result = 31 * result + maxVideoBitrate; result = 31 * result + (exceedVideoConstraintsIfNecessary ? 1 : 0); result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0); result = 31 * result + (orientationMayChange ? 1 : 0); @@ -406,7 +428,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { case C.TRACK_TYPE_VIDEO: rendererTrackSelections[i] = selectVideoTrack(rendererCapabilities[i], rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth, - params.maxVideoHeight, params.allowNonSeamlessAdaptiveness, + params.maxVideoHeight, params.maxVideoBitrate, params.allowNonSeamlessAdaptiveness, params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight, params.orientationMayChange, adaptiveVideoTrackSelectionFactory, params.exceedVideoConstraintsIfNecessary, @@ -436,30 +458,30 @@ public class DefaultTrackSelector extends MappingTrackSelector { protected TrackSelection selectVideoTrack(RendererCapabilities rendererCapabilities, TrackGroupArray groups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, - boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, int viewportWidth, - int viewportHeight, boolean orientationMayChange, + int maxVideoBitrate, boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, + int viewportWidth, int viewportHeight, boolean orientationMayChange, TrackSelection.Factory adaptiveVideoTrackSelectionFactory, boolean exceedConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary) throws ExoPlaybackException { TrackSelection selection = null; if (adaptiveVideoTrackSelectionFactory != null) { selection = selectAdaptiveVideoTrack(rendererCapabilities, groups, formatSupport, - maxVideoWidth, maxVideoHeight, allowNonSeamlessAdaptiveness, + maxVideoWidth, maxVideoHeight, maxVideoBitrate, allowNonSeamlessAdaptiveness, allowMixedMimeAdaptiveness, viewportWidth, viewportHeight, orientationMayChange, adaptiveVideoTrackSelectionFactory); } if (selection == null) { selection = selectFixedVideoTrack(groups, formatSupport, maxVideoWidth, maxVideoHeight, - viewportWidth, viewportHeight, orientationMayChange, exceedConstraintsIfNecessary, - exceedRendererCapabilitiesIfNecessary); + maxVideoBitrate, viewportWidth, viewportHeight, orientationMayChange, + exceedConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary); } return selection; } private static TrackSelection selectAdaptiveVideoTrack(RendererCapabilities rendererCapabilities, TrackGroupArray groups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, - boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, int viewportWidth, - int viewportHeight, boolean orientationMayChange, + int maxVideoBitrate, boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, + int viewportWidth, int viewportHeight, boolean orientationMayChange, TrackSelection.Factory adaptiveVideoTrackSelectionFactory) throws ExoPlaybackException { int requiredAdaptiveSupport = allowNonSeamlessAdaptiveness ? (RendererCapabilities.ADAPTIVE_NOT_SEAMLESS | RendererCapabilities.ADAPTIVE_SEAMLESS) @@ -470,7 +492,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup group = groups.get(i); int[] adaptiveTracks = getAdaptiveTracksForGroup(group, formatSupport[i], allowMixedMimeTypes, requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight, - viewportWidth, viewportHeight, orientationMayChange); + maxVideoBitrate, viewportWidth, viewportHeight, orientationMayChange); if (adaptiveTracks.length > 0) { return adaptiveVideoTrackSelectionFactory.createTrackSelection(group, adaptiveTracks); } @@ -480,7 +502,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static int[] getAdaptiveTracksForGroup(TrackGroup group, int[] formatSupport, boolean allowMixedMimeTypes, int requiredAdaptiveSupport, int maxVideoWidth, - int maxVideoHeight, int viewportWidth, int viewportHeight, boolean orientationMayChange) { + int maxVideoHeight, int maxVideoBitrate, int viewportWidth, int viewportHeight, + boolean orientationMayChange) { if (group.length < 2) { return NO_TRACKS; } @@ -499,11 +522,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { for (int i = 0; i < selectedTrackIndices.size(); i++) { int trackIndex = selectedTrackIndices.get(i); String sampleMimeType = group.getFormat(trackIndex).sampleMimeType; - if (!seenMimeTypes.contains(sampleMimeType)) { - seenMimeTypes.add(sampleMimeType); + if (seenMimeTypes.add(sampleMimeType)) { int countForMimeType = getAdaptiveTrackCountForMimeType(group, formatSupport, requiredAdaptiveSupport, sampleMimeType, maxVideoWidth, maxVideoHeight, - selectedTrackIndices); + maxVideoBitrate, selectedTrackIndices); if (countForMimeType > selectedMimeTypeTrackCount) { selectedMimeType = sampleMimeType; selectedMimeTypeTrackCount = countForMimeType; @@ -514,19 +536,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Filter by the selected mime type. filterAdaptiveTrackCountForMimeType(group, formatSupport, requiredAdaptiveSupport, - selectedMimeType, maxVideoWidth, maxVideoHeight, selectedTrackIndices); + selectedMimeType, maxVideoWidth, maxVideoHeight, maxVideoBitrate, selectedTrackIndices); return selectedTrackIndices.size() < 2 ? NO_TRACKS : Util.toArray(selectedTrackIndices); } private static int getAdaptiveTrackCountForMimeType(TrackGroup group, int[] formatSupport, int requiredAdaptiveSupport, String mimeType, int maxVideoWidth, int maxVideoHeight, - List selectedTrackIndices) { + int maxVideoBitrate, List selectedTrackIndices) { int adaptiveTrackCount = 0; for (int i = 0; i < selectedTrackIndices.size(); i++) { int trackIndex = selectedTrackIndices.get(i); if (isSupportedAdaptiveVideoTrack(group.getFormat(trackIndex), mimeType, - formatSupport[trackIndex], requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight)) { + formatSupport[trackIndex], requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight, + maxVideoBitrate)) { adaptiveTrackCount++; } } @@ -535,28 +558,31 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static void filterAdaptiveTrackCountForMimeType(TrackGroup group, int[] formatSupport, int requiredAdaptiveSupport, String mimeType, int maxVideoWidth, int maxVideoHeight, - List selectedTrackIndices) { + int maxVideoBitrate, List selectedTrackIndices) { for (int i = selectedTrackIndices.size() - 1; i >= 0; i--) { int trackIndex = selectedTrackIndices.get(i); if (!isSupportedAdaptiveVideoTrack(group.getFormat(trackIndex), mimeType, - formatSupport[trackIndex], requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight)) { + formatSupport[trackIndex], requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight, + maxVideoBitrate)) { selectedTrackIndices.remove(i); } } } private static boolean isSupportedAdaptiveVideoTrack(Format format, String mimeType, - int formatSupport, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight) { + int formatSupport, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight, + int maxVideoBitrate) { return isSupported(formatSupport, false) && ((formatSupport & requiredAdaptiveSupport) != 0) && (mimeType == null || Util.areEqual(format.sampleMimeType, mimeType)) && (format.width == Format.NO_VALUE || format.width <= maxVideoWidth) - && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight); + && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight) + && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxVideoBitrate); } private static TrackSelection selectFixedVideoTrack(TrackGroupArray groups, - int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, int viewportWidth, - int viewportHeight, boolean orientationMayChange, boolean exceedConstraintsIfNecessary, - boolean exceedRendererCapabilitiesIfNecessary) { + int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate, + int viewportWidth, int viewportHeight, boolean orientationMayChange, + boolean exceedConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -572,7 +598,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { Format format = trackGroup.getFormat(trackIndex); boolean isWithinConstraints = selectedTrackIndices.contains(trackIndex) && (format.width == Format.NO_VALUE || format.width <= maxVideoWidth) - && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight); + && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight) + && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxVideoBitrate); if (!isWithinConstraints && !exceedConstraintsIfNecessary) { // Track should not be selected. continue; From cda1b7b42b3c2bd5ea7bb5ccbd55234f7523cf57 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 7 Mar 2017 07:15:55 -0800 Subject: [PATCH 100/140] Try and get people to stop ignoring the issue template. Again. I'm also going to propose some canned responses that we can copy/paste into issues that ignore the template, so that we can be consistent about how we handle them. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=149415502 --- ISSUE_TEMPLATE | 59 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index 6e55f3dcd6..1b912312d1 100644 --- a/ISSUE_TEMPLATE +++ b/ISSUE_TEMPLATE @@ -1,19 +1,44 @@ -*** PLEASE DO NOT IGNORE THIS ISSUE TEMPLATE *** +*** ISSUES THAT IGNORE THIS TEMPLATE WILL BE CLOSED WITHOUT INVESTIGATION *** -Please search the existing issues before filing a new one, including issues that -are closed. When filing a new issue please include ALL of the following, unless -you're certain that they're not useful for the particular issue being reported. +Before filing an issue: +----------------------- +- Search existing issues, including issues that are closed. +- Consult our FAQs, supported devices and supported formats pages. These can be + found at https://google.github.io/ExoPlayer/. +- Rule out issues in your own code. A good way to do this is to try and + reproduce the issue in the ExoPlayer demo app. +- This issue tracker is intended for bugs, feature requests and ExoPlayer + specific questions. If you're asking a general Android development question, + please do so on Stack Overflow. -- A description of the issue. -- Steps describing how the issue can be reproduced, ideally in the ExoPlayer - demo app. -- A link to content that reproduces the issue. If you don't wish to post it - publicly, please submit the issue, then email the link to - dev.exoplayer@gmail.com including the issue number in the subject line. -- The version of ExoPlayer being used. -- The device(s) and version(s) of Android on which the issue can be reproduced, - and how easily it reproduces. If possible, please test on multiple devices and - Android versions. -- A bug report taken from the device just after the issue occurs, attached as a - file. A bug report can be captured using "adb bugreport". Output from "adb - logcat" or a log snippet is not sufficient. +When reporting a bug: +----------------------- +Fill out the sections below, leaving the headers but replacing the content. If +you're unable to provide certain information, please explain why in the relevant +section. We may close issues if they do not include sufficient information. + +### Issue description +Describe the issue in detail, including observed and expected behavior. + +### Reproduction steps +Describe how the issue can be reproduced, ideally using the ExoPlayer demo app. + +### Link to test content +Provide a link to media that reproduces the issue. If you don't wish to post it +publicly, please submit the issue, then email the link to +dev.exoplayer@gmail.com including the issue number in the subject line. + +### Version of ExoPlayer being used +Specify the absolute version number. Avoid using terms such as "latest". + +### Device(s) and version(s) of Android being used +Specify the devices and versions of Android on which the issue can be +reproduced, and how easily it reproduces. If possible, please test on multiple +devices and Android versions. + +### A full bug report captured from the device +Capture a full bug report using "adb bugreport". Output from "adb logcat" or a +log snippet is NOT sufficient. Please attach the captured bug report as a file. +If you don't wish to post it publicly, please submit the issue, then email the +bug report to dev.exoplayer@gmail.com including the issue number in the subject +line. From 99e19a92af8641c24470a01031652f40eef80253 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 7 Mar 2017 10:06:42 -0800 Subject: [PATCH 101/140] Fix SampleStream javadoc. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=149431794 --- .../com/google/android/exoplayer2/source/SampleStream.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SampleStream.java b/library/src/main/java/com/google/android/exoplayer2/source/SampleStream.java index 90153d1790..e3039878f8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/SampleStream.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/SampleStream.java @@ -29,7 +29,8 @@ public interface SampleStream { * Returns whether data is available to be read. *

      * Note: If the stream has ended then a buffer with the end of stream flag can always be read from - * {@link #readData(FormatHolder, DecoderInputBuffer)}. Hence an ended stream is always ready. + * {@link #readData(FormatHolder, DecoderInputBuffer, boolean)}. Hence an ended stream is always + * ready. * * @return Whether data is available to be read. */ From 09471defd72e7416f7560a926a335421f6378b40 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 8 Mar 2017 04:09:05 -0800 Subject: [PATCH 102/140] Enabled EMSG and CEA-608 embedded streams for DASH Issue: #2362 Issue: #2176 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=149524412 --- .../source/chunk/BaseMediaChunkOutput.java | 2 +- .../source/chunk/ChunkSampleStream.java | 248 +++++++++++++----- .../source/dash/DashMediaPeriod.java | 142 ++++++---- .../source/smoothstreaming/SsMediaPeriod.java | 4 +- 4 files changed, 283 insertions(+), 113 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java index a429a7cab9..3882a330f9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java @@ -35,7 +35,7 @@ import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOut * @param trackTypes The track types of the individual track outputs. * @param trackOutputs The individual track outputs. */ - public BaseMediaChunkOutput(int[] trackTypes, DefaultTrackOutput... trackOutputs) { + public BaseMediaChunkOutput(int[] trackTypes, DefaultTrackOutput[] trackOutputs) { this.trackTypes = trackTypes; this.trackOutputs = trackOutputs; } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 909bf317b3..1ae928045d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -33,31 +33,34 @@ import java.util.List; /** * A {@link SampleStream} that loads media in {@link Chunk}s, obtained from a {@link ChunkSource}. + * May also be configured to expose additional embedded {@link SampleStream}s. */ public class ChunkSampleStream implements SampleStream, SequenceableLoader, Loader.Callback { - private final int trackType; + private final int primaryTrackType; + private final int[] embeddedTrackTypes; private final T chunkSource; private final SequenceableLoader.Callback> callback; private final EventDispatcher eventDispatcher; private final int minLoadableRetryCount; + private final Loader loader; + private final ChunkHolder nextChunkHolder; private final LinkedList mediaChunks; private final List readOnlyMediaChunks; + private final DefaultTrackOutput primarySampleQueue; + private final EmbeddedSampleStream[] embeddedSampleStreams; private final BaseMediaChunkOutput mediaChunkOutput; - private final DefaultTrackOutput sampleQueue; - private final ChunkHolder nextChunkHolder; - private final Loader loader; - private Format downstreamTrackFormat; - - private long lastSeekPositionUs; + private Format primaryDownstreamTrackFormat; private long pendingResetPositionUs; - - private boolean loadingFinished; + /* package */ long lastSeekPositionUs; + /* package */ boolean loadingFinished; /** - * @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants. + * @param primaryTrackType The type of the primary track. One of the {@link C} + * {@code TRACK_TYPE_*} constants. + * @param embeddedTrackTypes The types of any embedded tracks, or null. * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained. * @param callback An {@link Callback} for the stream. * @param allocator An {@link Allocator} from which allocations can be obtained. @@ -66,10 +69,11 @@ public class ChunkSampleStream implements SampleStream, S * before propagating an error. * @param eventDispatcher A dispatcher to notify of events. */ - public ChunkSampleStream(int trackType, T chunkSource, - SequenceableLoader.Callback> callback, Allocator allocator, - long positionUs, int minLoadableRetryCount, EventDispatcher eventDispatcher) { - this.trackType = trackType; + public ChunkSampleStream(int primaryTrackType, int[] embeddedTrackTypes, T chunkSource, + Callback> callback, Allocator allocator, long positionUs, + int minLoadableRetryCount, EventDispatcher eventDispatcher) { + this.primaryTrackType = primaryTrackType; + this.embeddedTrackTypes = embeddedTrackTypes; this.chunkSource = chunkSource; this.callback = callback; this.eventDispatcher = eventDispatcher; @@ -78,16 +82,56 @@ public class ChunkSampleStream implements SampleStream, S nextChunkHolder = new ChunkHolder(); mediaChunks = new LinkedList<>(); readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); - sampleQueue = new DefaultTrackOutput(allocator); - mediaChunkOutput = new BaseMediaChunkOutput(new int[] {trackType}, sampleQueue); - lastSeekPositionUs = positionUs; + + int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length; + embeddedSampleStreams = newEmbeddedSampleStreamArray(embeddedTrackCount); + int[] trackTypes = new int[1 + embeddedTrackCount]; + DefaultTrackOutput[] sampleQueues = new DefaultTrackOutput[1 + embeddedTrackCount]; + + primarySampleQueue = new DefaultTrackOutput(allocator); + trackTypes[0] = primaryTrackType; + sampleQueues[0] = primarySampleQueue; + + for (int i = 0; i < embeddedTrackCount; i++) { + trackTypes[i + 1] = embeddedTrackTypes[i]; + sampleQueues[i + 1] = new DefaultTrackOutput(allocator); + embeddedSampleStreams[i] = new EmbeddedSampleStream(sampleQueues[i + 1]); + } + + mediaChunkOutput = new BaseMediaChunkOutput(trackTypes, sampleQueues); pendingResetPositionUs = positionUs; + lastSeekPositionUs = positionUs; + } + + /** + * Returns whether a {@link SampleStream} is for an embedded track of a {@link ChunkSampleStream}. + */ + public static boolean isPrimarySampleStream(SampleStream sampleStream) { + return sampleStream instanceof ChunkSampleStream; + } + + /** + * Returns whether a {@link SampleStream} is for an embedded track of a {@link ChunkSampleStream}. + */ + public static boolean isEmbeddedSampleStream(SampleStream sampleStream) { + return sampleStream instanceof ChunkSampleStream.EmbeddedSampleStream; + } + + /** + * Returns the {@link SampleStream} for the embedded track with the specified type. + */ + public SampleStream getEmbeddedSampleStream(int trackType) { + for (int i = 0; i < embeddedTrackTypes.length; i++) { + if (embeddedTrackTypes[i] == trackType) { + return embeddedSampleStreams[i]; + } + } + // Should never happen. + throw new IllegalStateException(); } /** * Returns the {@link ChunkSource} used by this stream. - * - * @return The {@link ChunkSource}. */ public T getChunkSource() { return chunkSource; @@ -112,7 +156,7 @@ public class ChunkSampleStream implements SampleStream, S if (lastCompletedMediaChunk != null) { bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs); } - return Math.max(bufferedPositionUs, sampleQueue.getLargestQueuedTimestampUs()); + return Math.max(bufferedPositionUs, primarySampleQueue.getLargestQueuedTimestampUs()); } } @@ -123,15 +167,21 @@ public class ChunkSampleStream implements SampleStream, S */ public void seekToUs(long positionUs) { lastSeekPositionUs = positionUs; - // If we're not pending a reset, see if we can seek within the sample queue. - boolean seekInsideBuffer = !isPendingReset() - && sampleQueue.skipToKeyframeBefore(positionUs, positionUs < getNextLoadPositionUs()); + // If we're not pending a reset, see if we can seek within the primary sample queue. + boolean seekInsideBuffer = !isPendingReset() && primarySampleQueue.skipToKeyframeBefore( + positionUs, positionUs < getNextLoadPositionUs()); if (seekInsideBuffer) { - // We succeeded. All we need to do is discard any chunks that we've moved past. + // We succeeded. We need to discard any chunks that we've moved past and perform the seek for + // any embedded streams as well. while (mediaChunks.size() > 1 - && mediaChunks.get(1).getFirstSampleIndex(0) <= sampleQueue.getReadIndex()) { + && mediaChunks.get(1).getFirstSampleIndex(0) <= primarySampleQueue.getReadIndex()) { mediaChunks.removeFirst(); } + // TODO: For this to work correctly, the embedded streams must not discard anything from their + // sample queues beyond the current read position of the primary stream. + for (EmbeddedSampleStream embeddedSampleStream : embeddedSampleStreams) { + embeddedSampleStream.skipToKeyframeBefore(positionUs); + } } else { // We failed, and need to restart. pendingResetPositionUs = positionUs; @@ -140,7 +190,10 @@ public class ChunkSampleStream implements SampleStream, S if (loader.isLoading()) { loader.cancelLoading(); } else { - sampleQueue.reset(true); + primarySampleQueue.reset(true); + for (EmbeddedSampleStream embeddedSampleStream : embeddedSampleStreams) { + embeddedSampleStream.reset(true); + } } } } @@ -151,7 +204,10 @@ public class ChunkSampleStream implements SampleStream, S * This method should be called when the stream is no longer required. */ public void release() { - sampleQueue.disable(); + primarySampleQueue.disable(); + for (EmbeddedSampleStream embeddedSampleStream : embeddedSampleStreams) { + embeddedSampleStream.disable(); + } loader.release(); } @@ -159,7 +215,7 @@ public class ChunkSampleStream implements SampleStream, S @Override public boolean isReady() { - return loadingFinished || (!isPendingReset() && !sampleQueue.isEmpty()); + return loadingFinished || (!isPendingReset() && !primarySampleQueue.isEmpty()); } @Override @@ -176,27 +232,15 @@ public class ChunkSampleStream implements SampleStream, S if (isPendingReset()) { return C.RESULT_NOTHING_READ; } - - while (mediaChunks.size() > 1 - && mediaChunks.get(1).getFirstSampleIndex(0) <= sampleQueue.getReadIndex()) { - mediaChunks.removeFirst(); - } - BaseMediaChunk currentChunk = mediaChunks.getFirst(); - - Format trackFormat = currentChunk.trackFormat; - if (!trackFormat.equals(downstreamTrackFormat)) { - eventDispatcher.downstreamFormatChanged(trackType, trackFormat, - currentChunk.trackSelectionReason, currentChunk.trackSelectionData, - currentChunk.startTimeUs); - } - downstreamTrackFormat = trackFormat; - return sampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, + // TODO: For embedded streams that aren't being used, we need to drain their queues here. + discardDownstreamMediaChunks(primarySampleQueue.getReadIndex()); + return primarySampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); } @Override public void skipToKeyframeBefore(long timeUs) { - sampleQueue.skipToKeyframeBefore(timeUs); + primarySampleQueue.skipToKeyframeBefore(timeUs); } // Loader.Callback implementation. @@ -204,20 +248,25 @@ public class ChunkSampleStream implements SampleStream, S @Override public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) { chunkSource.onChunkLoadCompleted(loadable); - eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, - loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); + eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, primaryTrackType, + loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, + loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, + loadable.bytesLoaded()); callback.onContinueLoadingRequested(this); } @Override public void onLoadCanceled(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { - eventDispatcher.loadCanceled(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, - loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); + eventDispatcher.loadCanceled(loadable.dataSpec, loadable.type, primaryTrackType, + loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, + loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, + loadable.bytesLoaded()); if (!released) { - sampleQueue.reset(true); + primarySampleQueue.reset(true); + for (EmbeddedSampleStream embeddedStream : embeddedSampleStreams) { + embeddedStream.reset(true); + } callback.onContinueLoadingRequested(this); } } @@ -234,16 +283,19 @@ public class ChunkSampleStream implements SampleStream, S if (isMediaChunk) { BaseMediaChunk removed = mediaChunks.removeLast(); Assertions.checkState(removed == loadable); - sampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); + primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); + for (int i = 0; i < embeddedSampleStreams.length; i++) { + embeddedSampleStreams[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); + } if (mediaChunks.isEmpty()) { pendingResetPositionUs = lastSeekPositionUs; } } } - eventDispatcher.loadError(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, - loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, bytesLoaded, error, - canceled); + eventDispatcher.loadError(loadable.dataSpec, loadable.type, primaryTrackType, + loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, + loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, bytesLoaded, + error, canceled); if (canceled) { callback.onContinueLoadingRequested(this); return Loader.DONT_RETRY; @@ -283,9 +335,9 @@ public class ChunkSampleStream implements SampleStream, S mediaChunks.add(mediaChunk); } long elapsedRealtimeMs = loader.startLoading(loadable, this, minLoadableRetryCount); - eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, - loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, elapsedRealtimeMs); + eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, primaryTrackType, + loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, + loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs); return true; } @@ -316,10 +368,25 @@ public class ChunkSampleStream implements SampleStream, S return chunk instanceof BaseMediaChunk; } - private boolean isPendingReset() { + /* package */ boolean isPendingReset() { return pendingResetPositionUs != C.TIME_UNSET; } + private void discardDownstreamMediaChunks(int primaryStreamReadIndex) { + while (mediaChunks.size() > 1 + && mediaChunks.get(1).getFirstSampleIndex(0) <= primaryStreamReadIndex) { + mediaChunks.removeFirst(); + } + BaseMediaChunk currentChunk = mediaChunks.getFirst(); + Format trackFormat = currentChunk.trackFormat; + if (!trackFormat.equals(primaryDownstreamTrackFormat)) { + eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat, + currentChunk.trackSelectionReason, currentChunk.trackSelectionData, + currentChunk.startTimeUs); + } + primaryDownstreamTrackFormat = trackFormat; + } + /** * Discard upstream media chunks until the queue length is equal to the length specified. * @@ -332,16 +399,71 @@ public class ChunkSampleStream implements SampleStream, S } long startTimeUs = 0; long endTimeUs = mediaChunks.getLast().endTimeUs; - BaseMediaChunk removed = null; while (mediaChunks.size() > queueLength) { removed = mediaChunks.removeLast(); startTimeUs = removed.startTimeUs; loadingFinished = false; } - sampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); - eventDispatcher.upstreamDiscarded(trackType, startTimeUs, endTimeUs); + primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); + for (int i = 0; i < embeddedSampleStreams.length; i++) { + embeddedSampleStreams[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); + } + eventDispatcher.upstreamDiscarded(primaryTrackType, startTimeUs, endTimeUs); return true; } + @SuppressWarnings("unchecked") + private static ChunkSampleStream.EmbeddedSampleStream[] + newEmbeddedSampleStreamArray(int length) { + return new ChunkSampleStream.EmbeddedSampleStream[length]; + } + + private final class EmbeddedSampleStream implements SampleStream { + + private final DefaultTrackOutput sampleQueue; + + public EmbeddedSampleStream(DefaultTrackOutput sampleQueue) { + this.sampleQueue = sampleQueue; + } + + @Override + public boolean isReady() { + return loadingFinished || (!isPendingReset() && !sampleQueue.isEmpty()); + } + + @Override + public void skipToKeyframeBefore(long timeUs) { + sampleQueue.skipToKeyframeBefore(timeUs); + } + + @Override + public void maybeThrowError() throws IOException { + // Do nothing. Errors will be thrown from the primary stream. + } + + @Override + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { + if (isPendingReset()) { + return C.RESULT_NOTHING_READ; + } + return sampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, + lastSeekPositionUs); + } + + public void reset(boolean enable) { + sampleQueue.reset(enable); + } + + public void disable() { + sampleQueue.disable(); + } + + public void discardUpstreamSamples(int discardFromIndex) { + sampleQueue.discardUpstreamSamples(discardFromIndex); + } + + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index e89deb53ab..8905607bc1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.dash; +import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; @@ -35,7 +36,8 @@ import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; -import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; /** @@ -52,6 +54,7 @@ import java.util.List; private final LoaderErrorThrower manifestLoaderErrorThrower; private final Allocator allocator; private final TrackGroupArray trackGroups; + private final EmbeddedTrackInfo[] embeddedTrackInfos; private Callback callback; private ChunkSampleStream[] sampleStreams; @@ -76,7 +79,9 @@ import java.util.List; sampleStreams = newSampleStreamArray(0); sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); adaptationSets = manifest.getPeriod(periodIndex).adaptationSets; - trackGroups = buildTrackGroups(adaptationSets); + Pair result = buildTrackGroups(adaptationSets); + trackGroups = result.first; + embeddedTrackInfos = result.second; } public void updateManifest(DashManifest manifest, int periodIndex) { @@ -116,37 +121,59 @@ import java.util.List; @Override public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - ArrayList> sampleStreamsList = new ArrayList<>(); + int adaptationSetCount = adaptationSets.size(); + HashMap> primarySampleStreams = new HashMap<>(); + // First pass for primary tracks. for (int i = 0; i < selections.length; i++) { - if (streams[i] instanceof ChunkSampleStream) { + if (ChunkSampleStream.isPrimarySampleStream(streams[i])) { @SuppressWarnings("unchecked") ChunkSampleStream stream = (ChunkSampleStream) streams[i]; if (selections[i] == null || !mayRetainStreamFlags[i]) { stream.release(); streams[i] = null; } else { - sampleStreamsList.add(stream); + int adaptationSetIndex = trackGroups.indexOf(selections[i].getTrackGroup()); + primarySampleStreams.put(adaptationSetIndex, stream); } - } else if (streams[i] instanceof EmptySampleStream && selections[i] == null) { - // TODO: Release streams for cea-608 and emsg tracks. + } + if (streams[i] == null && selections[i] != null) { + int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); + if (trackGroupIndex < adaptationSetCount) { + ChunkSampleStream stream = buildSampleStream(trackGroupIndex, + selections[i], positionUs); + primarySampleStreams.put(trackGroupIndex, stream); + streams[i] = stream; + streamResetFlags[i] = true; + } + } + } + // Second pass for embedded tracks. + for (int i = 0; i < selections.length; i++) { + if (ChunkSampleStream.isEmbeddedSampleStream(streams[i])) { + // Always clear even if the selection is unchanged, since the parent primary sample stream + // may have been replaced. streams[i] = null; } if (streams[i] == null && selections[i] != null) { - int adaptationSetIndex = trackGroups.indexOf(selections[i].getTrackGroup()); - if (adaptationSetIndex < adaptationSets.size()) { - ChunkSampleStream stream = buildSampleStream(adaptationSetIndex, - selections[i], positionUs); - sampleStreamsList.add(stream); - streams[i] = stream; - } else { - // TODO: Output streams for cea-608 and emsg tracks. - streams[i] = new EmptySampleStream(); + int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); + if (trackGroupIndex >= adaptationSetCount) { + EmbeddedTrackInfo embeddedTrackInfo = + embeddedTrackInfos[trackGroupIndex - adaptationSetCount]; + int adaptationSetIndex = embeddedTrackInfo.adaptationSetIndex; + ChunkSampleStream primarySampleStream = + primarySampleStreams.get(adaptationSetIndex); + if (primarySampleStream != null) { + streams[i] = primarySampleStream.getEmbeddedSampleStream(embeddedTrackInfo.trackType); + } else { + // The primary track in which this one is embedded is not selected. + streams[i] = new EmptySampleStream(); + } + streamResetFlags[i] = true; } - streamResetFlags[i] = true; } } - sampleStreams = newSampleStreamArray(sampleStreamsList.size()); - sampleStreamsList.toArray(sampleStreams); + sampleStreams = newSampleStreamArray(primarySampleStreams.size()); + primarySampleStreams.values().toArray(sampleStreams); sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); return positionUs; } @@ -195,14 +222,14 @@ import java.util.List; // Internal methods. - private static TrackGroupArray buildTrackGroups(List adaptationSets) { + private static Pair buildTrackGroups( + List adaptationSets) { int adaptationSetCount = adaptationSets.size(); - int eventMessageTrackCount = getEventMessageTrackCount(adaptationSets); - int cea608TrackCount = getCea608TrackCount(adaptationSets); - TrackGroup[] trackGroupArray = new TrackGroup[adaptationSetCount + eventMessageTrackCount - + cea608TrackCount]; - int eventMessageTrackIndex = 0; - int cea608TrackIndex = 0; + int embeddedTrackCount = getEmbeddedTrackCount(adaptationSets); + TrackGroup[] trackGroupArray = new TrackGroup[adaptationSetCount + embeddedTrackCount]; + EmbeddedTrackInfo[] embeddedTrackInfos = new EmbeddedTrackInfo[embeddedTrackCount]; + + int embeddedTrackIndex = 0; for (int i = 0; i < adaptationSetCount; i++) { AdaptationSet adaptationSet = adaptationSets.get(i); List representations = adaptationSet.representations; @@ -214,38 +241,57 @@ import java.util.List; if (hasEventMessageTrack(adaptationSet)) { Format format = Format.createSampleFormat(adaptationSet.id + ":emsg", MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null); - trackGroupArray[adaptationSetCount + eventMessageTrackIndex++] = new TrackGroup(format); + trackGroupArray[adaptationSetCount + embeddedTrackIndex] = new TrackGroup(format); + embeddedTrackInfos[embeddedTrackIndex++] = new EmbeddedTrackInfo(i, C.TRACK_TYPE_METADATA); } if (hasCea608Track(adaptationSet)) { Format format = Format.createTextSampleFormat(adaptationSet.id + ":cea608", MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null); - trackGroupArray[adaptationSetCount + eventMessageTrackCount + cea608TrackIndex++] = - new TrackGroup(format); + trackGroupArray[adaptationSetCount + embeddedTrackIndex] = new TrackGroup(format); + embeddedTrackInfos[embeddedTrackIndex++] = new EmbeddedTrackInfo(i, C.TRACK_TYPE_TEXT); } } - return new TrackGroupArray(trackGroupArray); + + return Pair.create(new TrackGroupArray(trackGroupArray), embeddedTrackInfos); } private ChunkSampleStream buildSampleStream(int adaptationSetIndex, TrackSelection selection, long positionUs) { AdaptationSet adaptationSet = adaptationSets.get(adaptationSetIndex); + int embeddedTrackCount = 0; + int[] embeddedTrackTypes = new int[2]; boolean enableEventMessageTrack = hasEventMessageTrack(adaptationSet); + if (enableEventMessageTrack) { + embeddedTrackTypes[embeddedTrackCount++] = C.TRACK_TYPE_METADATA; + } boolean enableCea608Track = hasCea608Track(adaptationSet); + if (enableCea608Track) { + embeddedTrackTypes[embeddedTrackCount++] = C.TRACK_TYPE_TEXT; + } + if (embeddedTrackCount < embeddedTrackTypes.length) { + embeddedTrackTypes = Arrays.copyOf(embeddedTrackTypes, embeddedTrackCount); + } DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource( manifestLoaderErrorThrower, manifest, periodIndex, adaptationSetIndex, selection, elapsedRealtimeOffset, enableEventMessageTrack, enableCea608Track); - return new ChunkSampleStream<>(adaptationSet.type, chunkSource, this, allocator, positionUs, - minLoadableRetryCount, eventDispatcher); + ChunkSampleStream stream = new ChunkSampleStream<>(adaptationSet.type, + embeddedTrackTypes, chunkSource, this, allocator, positionUs, minLoadableRetryCount, + eventDispatcher); + return stream; } - private static int getEventMessageTrackCount(List adaptationSets) { - int inbandEventStreamTrackCount = 0; + private static int getEmbeddedTrackCount(List adaptationSets) { + int embeddedTrackCount = 0; for (int i = 0; i < adaptationSets.size(); i++) { - if (hasEventMessageTrack(adaptationSets.get(i))) { - inbandEventStreamTrackCount++; + AdaptationSet adaptationSet = adaptationSets.get(i); + if (hasEventMessageTrack(adaptationSet)) { + embeddedTrackCount++; + } + if (hasCea608Track(adaptationSet)) { + embeddedTrackCount++; } } - return inbandEventStreamTrackCount; + return embeddedTrackCount; } private static boolean hasEventMessageTrack(AdaptationSet adaptationSet) { @@ -259,16 +305,6 @@ import java.util.List; return false; } - private static int getCea608TrackCount(List adaptationSets) { - int cea608TrackCount = 0; - for (int i = 0; i < adaptationSets.size(); i++) { - if (hasCea608Track(adaptationSets.get(i))) { - cea608TrackCount++; - } - } - return cea608TrackCount; - } - private static boolean hasCea608Track(AdaptationSet adaptationSet) { List descriptors = adaptationSet.accessibilityDescriptors; for (int i = 0; i < descriptors.size(); i++) { @@ -285,4 +321,16 @@ import java.util.List; return new ChunkSampleStream[length]; } + private static final class EmbeddedTrackInfo { + + public final int adaptationSetIndex; + public final int trackType; + + public EmbeddedTrackInfo(int adaptationSetIndex, int trackType) { + this.adaptationSetIndex = adaptationSetIndex; + this.trackType = trackType; + } + + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index fef2480fd6..b9af9930dc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -185,8 +185,8 @@ import java.util.ArrayList; int streamElementIndex = trackGroups.indexOf(selection.getTrackGroup()); SsChunkSource chunkSource = chunkSourceFactory.createChunkSource(manifestLoaderErrorThrower, manifest, streamElementIndex, selection, trackEncryptionBoxes); - return new ChunkSampleStream<>(manifest.streamElements[streamElementIndex].type, chunkSource, - this, allocator, positionUs, minLoadableRetryCount, eventDispatcher); + return new ChunkSampleStream<>(manifest.streamElements[streamElementIndex].type, null, + chunkSource, this, allocator, positionUs, minLoadableRetryCount, eventDispatcher); } private static TrackGroupArray buildTrackGroups(SsManifest manifest) { From 45c7fe9b00e0cc9068c245ba001dd67c420f12ab Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 8 Mar 2017 04:37:47 -0800 Subject: [PATCH 103/140] Drain embedded track sample queues when not enabled. I think it's likely we'll revert back to discarding media in sync with the playback position for ExtractorMediaSource and HlsMediaSource too, where the tracks are muxed with ones we're requesting anyway. Note: discardBuffer is named as it is because it'll also be used to discard for enabled tracks soon, as a result of the remaining TODO in ChunkSampleStream. For enabled tracks the discard will also be conditional on the samples having been consumed, obviously. Issue: #2362 Issue: #2176 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=149525857 --- .../android/exoplayer2/ExoPlayerTest.java | 5 + .../exoplayer2/ExoPlayerImplInternal.java | 2 + .../source/ClippingMediaPeriod.java | 5 + .../source/ExtractorMediaPeriod.java | 5 + .../exoplayer2/source/MediaPeriod.java | 7 ++ .../exoplayer2/source/MergingMediaPeriod.java | 7 ++ .../source/SingleSampleMediaPeriod.java | 5 + .../source/chunk/ChunkSampleStream.java | 104 ++++++++++-------- .../source/dash/DashMediaPeriod.java | 49 ++++++--- .../exoplayer2/source/hls/HlsMediaPeriod.java | 5 + .../source/smoothstreaming/SsMediaPeriod.java | 5 + 11 files changed, 136 insertions(+), 63 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 2ad1159c3e..93c0a7dc11 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -460,6 +460,11 @@ public final class ExoPlayerTest extends TestCase { return 0; } + @Override + public void discardBuffer(long positionUs) { + // Do nothing. + } + @Override public long readDiscontinuity() { assertTrue(preparedPeriod); diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index faf86087c9..e4c109e85b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -455,6 +455,8 @@ import java.io.IOException; TraceUtil.beginSection("doSomeWork"); updatePlaybackPositions(); + playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs); + boolean allRenderersEnded = true; boolean allRenderersReadyOrEnded = true; for (Renderer renderer : enabledRenderers) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index 51663a21c6..102a689742 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -109,6 +109,11 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb return enablePositionUs - startUs; } + @Override + public void discardBuffer(long positionUs) { + mediaPeriod.discardBuffer(positionUs + startUs); + } + @Override public long readDiscontinuity() { if (pendingInitialDiscontinuity) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 97e9ddd7e7..31b76a84b3 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -234,6 +234,11 @@ import java.io.IOException; return positionUs; } + @Override + public void discardBuffer(long positionUs) { + // Do nothing. + } + @Override public boolean continueLoading(long playbackPositionUs) { if (loadingFinished || (prepared && enabledTrackCount == 0)) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index 31ee8df1e4..3b06542855 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -104,6 +104,13 @@ public interface MediaPeriod extends SequenceableLoader { long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs); + /** + * Discards buffered media up to the specified position. + * + * @param positionUs The position in microseconds. + */ + void discardBuffer(long positionUs); + /** * Attempts to read a discontinuity. *

      diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java index 10c56e5576..077b5576c1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -128,6 +128,13 @@ import java.util.IdentityHashMap; return positionUs; } + @Override + public void discardBuffer(long positionUs) { + for (MediaPeriod period : enabledPeriods) { + period.discardBuffer(positionUs); + } + } + @Override public boolean continueLoading(long positionUs) { return sequenceableLoader.continueLoading(positionUs); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index fd2ebffe8e..5b717e51da 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -111,6 +111,11 @@ import java.util.Arrays; return positionUs; } + @Override + public void discardBuffer(long positionUs) { + // Do nothing. + } + @Override public boolean continueLoading(long positionUs) { if (loadingFinished || loader.isLoading()) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 1ae928045d..93d86a8de1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -40,6 +40,7 @@ public class ChunkSampleStream implements SampleStream, S private final int primaryTrackType; private final int[] embeddedTrackTypes; + private final boolean[] embeddedTracksSelected; private final T chunkSource; private final SequenceableLoader.Callback> callback; private final EventDispatcher eventDispatcher; @@ -49,7 +50,7 @@ public class ChunkSampleStream implements SampleStream, S private final LinkedList mediaChunks; private final List readOnlyMediaChunks; private final DefaultTrackOutput primarySampleQueue; - private final EmbeddedSampleStream[] embeddedSampleStreams; + private final DefaultTrackOutput[] embeddedSampleQueues; private final BaseMediaChunkOutput mediaChunkOutput; private Format primaryDownstreamTrackFormat; @@ -84,7 +85,8 @@ public class ChunkSampleStream implements SampleStream, S readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length; - embeddedSampleStreams = newEmbeddedSampleStreamArray(embeddedTrackCount); + embeddedSampleQueues = new DefaultTrackOutput[embeddedTrackCount]; + embeddedTracksSelected = new boolean[embeddedTrackCount]; int[] trackTypes = new int[1 + embeddedTrackCount]; DefaultTrackOutput[] sampleQueues = new DefaultTrackOutput[1 + embeddedTrackCount]; @@ -93,9 +95,10 @@ public class ChunkSampleStream implements SampleStream, S sampleQueues[0] = primarySampleQueue; for (int i = 0; i < embeddedTrackCount; i++) { + DefaultTrackOutput trackOutput = new DefaultTrackOutput(allocator); + embeddedSampleQueues[i] = trackOutput; + sampleQueues[i + 1] = trackOutput; trackTypes[i + 1] = embeddedTrackTypes[i]; - sampleQueues[i + 1] = new DefaultTrackOutput(allocator); - embeddedSampleStreams[i] = new EmbeddedSampleStream(sampleQueues[i + 1]); } mediaChunkOutput = new BaseMediaChunkOutput(trackTypes, sampleQueues); @@ -104,26 +107,36 @@ public class ChunkSampleStream implements SampleStream, S } /** - * Returns whether a {@link SampleStream} is for an embedded track of a {@link ChunkSampleStream}. + * Discards buffered media for embedded tracks that are not currently selected, up to the + * specified position. + * + * @param positionUs The position to discard up to, in microseconds. */ - public static boolean isPrimarySampleStream(SampleStream sampleStream) { - return sampleStream instanceof ChunkSampleStream; + public void discardUnselectedEmbeddedTracksTo(long positionUs) { + for (int i = 0; i < embeddedSampleQueues.length; i++) { + if (!embeddedTracksSelected[i]) { + embeddedSampleQueues[i].skipToKeyframeBefore(positionUs, true); + } + } } /** - * Returns whether a {@link SampleStream} is for an embedded track of a {@link ChunkSampleStream}. + * Selects the embedded track, returning a new {@link EmbeddedSampleStream} from which the track's + * samples can be consumed. {@link EmbeddedSampleStream#release()} must be called on the returned + * stream when the track is no longer required, and before calling this method again to obtain + * another stream for the same track. + * + * @param positionUs The current playback position in microseconds. + * @param trackType The type of the embedded track to enable. + * @return The {@link EmbeddedSampleStream} for the embedded track. */ - public static boolean isEmbeddedSampleStream(SampleStream sampleStream) { - return sampleStream instanceof ChunkSampleStream.EmbeddedSampleStream; - } - - /** - * Returns the {@link SampleStream} for the embedded track with the specified type. - */ - public SampleStream getEmbeddedSampleStream(int trackType) { - for (int i = 0; i < embeddedTrackTypes.length; i++) { + public EmbeddedSampleStream selectEmbeddedTrack(long positionUs, int trackType) { + for (int i = 0; i < embeddedSampleQueues.length; i++) { if (embeddedTrackTypes[i] == trackType) { - return embeddedSampleStreams[i]; + Assertions.checkState(!embeddedTracksSelected[i]); + embeddedTracksSelected[i] = true; + embeddedSampleQueues[i].skipToKeyframeBefore(positionUs, true); + return new EmbeddedSampleStream(this, embeddedSampleQueues[i], i); } } // Should never happen. @@ -179,8 +192,8 @@ public class ChunkSampleStream implements SampleStream, S } // TODO: For this to work correctly, the embedded streams must not discard anything from their // sample queues beyond the current read position of the primary stream. - for (EmbeddedSampleStream embeddedSampleStream : embeddedSampleStreams) { - embeddedSampleStream.skipToKeyframeBefore(positionUs); + for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.skipToKeyframeBefore(positionUs); } } else { // We failed, and need to restart. @@ -191,8 +204,8 @@ public class ChunkSampleStream implements SampleStream, S loader.cancelLoading(); } else { primarySampleQueue.reset(true); - for (EmbeddedSampleStream embeddedSampleStream : embeddedSampleStreams) { - embeddedSampleStream.reset(true); + for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.reset(true); } } } @@ -205,8 +218,8 @@ public class ChunkSampleStream implements SampleStream, S */ public void release() { primarySampleQueue.disable(); - for (EmbeddedSampleStream embeddedSampleStream : embeddedSampleStreams) { - embeddedSampleStream.disable(); + for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.disable(); } loader.release(); } @@ -232,7 +245,6 @@ public class ChunkSampleStream implements SampleStream, S if (isPendingReset()) { return C.RESULT_NOTHING_READ; } - // TODO: For embedded streams that aren't being used, we need to drain their queues here. discardDownstreamMediaChunks(primarySampleQueue.getReadIndex()); return primarySampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); @@ -264,8 +276,8 @@ public class ChunkSampleStream implements SampleStream, S loadable.bytesLoaded()); if (!released) { primarySampleQueue.reset(true); - for (EmbeddedSampleStream embeddedStream : embeddedSampleStreams) { - embeddedStream.reset(true); + for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.reset(true); } callback.onContinueLoadingRequested(this); } @@ -284,8 +296,8 @@ public class ChunkSampleStream implements SampleStream, S BaseMediaChunk removed = mediaChunks.removeLast(); Assertions.checkState(removed == loadable); primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); - for (int i = 0; i < embeddedSampleStreams.length; i++) { - embeddedSampleStreams[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); + for (int i = 0; i < embeddedSampleQueues.length; i++) { + embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); } if (mediaChunks.isEmpty()) { pendingResetPositionUs = lastSeekPositionUs; @@ -406,25 +418,28 @@ public class ChunkSampleStream implements SampleStream, S loadingFinished = false; } primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); - for (int i = 0; i < embeddedSampleStreams.length; i++) { - embeddedSampleStreams[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); + for (int i = 0; i < embeddedSampleQueues.length; i++) { + embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); } eventDispatcher.upstreamDiscarded(primaryTrackType, startTimeUs, endTimeUs); return true; } - @SuppressWarnings("unchecked") - private static ChunkSampleStream.EmbeddedSampleStream[] - newEmbeddedSampleStreamArray(int length) { - return new ChunkSampleStream.EmbeddedSampleStream[length]; - } + /** + * A {@link SampleStream} embedded in a {@link ChunkSampleStream}. + */ + public final class EmbeddedSampleStream implements SampleStream { - private final class EmbeddedSampleStream implements SampleStream { + public final ChunkSampleStream parent; private final DefaultTrackOutput sampleQueue; + private final int index; - public EmbeddedSampleStream(DefaultTrackOutput sampleQueue) { + public EmbeddedSampleStream(ChunkSampleStream parent, DefaultTrackOutput sampleQueue, + int index) { + this.parent = parent; this.sampleQueue = sampleQueue; + this.index = index; } @Override @@ -452,16 +467,9 @@ public class ChunkSampleStream implements SampleStream, S lastSeekPositionUs); } - public void reset(boolean enable) { - sampleQueue.reset(enable); - } - - public void disable() { - sampleQueue.disable(); - } - - public void discardUpstreamSamples(int discardFromIndex) { - sampleQueue.discardUpstreamSamples(discardFromIndex); + public void release() { + Assertions.checkState(embeddedTracksSelected[index]); + embeddedTracksSelected[index] = false; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index 8905607bc1..5e0541cb31 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.chunk.ChunkSampleStream; +import com.google.android.exoplayer2.source.chunk.ChunkSampleStream.EmbeddedSampleStream; import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.Representation; @@ -125,7 +126,7 @@ import java.util.List; HashMap> primarySampleStreams = new HashMap<>(); // First pass for primary tracks. for (int i = 0; i < selections.length; i++) { - if (ChunkSampleStream.isPrimarySampleStream(streams[i])) { + if (streams[i] instanceof ChunkSampleStream) { @SuppressWarnings("unchecked") ChunkSampleStream stream = (ChunkSampleStream) streams[i]; if (selections[i] == null || !mayRetainStreamFlags[i]) { @@ -149,26 +150,31 @@ import java.util.List; } // Second pass for embedded tracks. for (int i = 0; i < selections.length; i++) { - if (ChunkSampleStream.isEmbeddedSampleStream(streams[i])) { - // Always clear even if the selection is unchanged, since the parent primary sample stream - // may have been replaced. + if ((streams[i] instanceof EmbeddedSampleStream || streams[i] instanceof EmptySampleStream) + && (selections[i] == null || !mayRetainStreamFlags[i])) { + // The stream is for an embedded track and is either no longer selected or needs replacing. + releaseIfEmbeddedSampleStream(streams[i]); streams[i] = null; } - if (streams[i] == null && selections[i] != null) { + // We need to consider replacing the stream even if it's non-null because the primary stream + // may have been replaced, selected or deselected. + if (selections[i] != null) { int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); if (trackGroupIndex >= adaptationSetCount) { - EmbeddedTrackInfo embeddedTrackInfo = - embeddedTrackInfos[trackGroupIndex - adaptationSetCount]; + int embeddedTrackIndex = trackGroupIndex - adaptationSetCount; + EmbeddedTrackInfo embeddedTrackInfo = embeddedTrackInfos[embeddedTrackIndex]; int adaptationSetIndex = embeddedTrackInfo.adaptationSetIndex; - ChunkSampleStream primarySampleStream = - primarySampleStreams.get(adaptationSetIndex); - if (primarySampleStream != null) { - streams[i] = primarySampleStream.getEmbeddedSampleStream(embeddedTrackInfo.trackType); - } else { - // The primary track in which this one is embedded is not selected. - streams[i] = new EmptySampleStream(); + ChunkSampleStream primaryStream = primarySampleStreams.get(adaptationSetIndex); + SampleStream stream = streams[i]; + boolean mayRetainStream = primaryStream == null ? stream instanceof EmptySampleStream + : (stream instanceof EmbeddedSampleStream + && ((EmbeddedSampleStream) stream).parent == primaryStream); + if (!mayRetainStream) { + releaseIfEmbeddedSampleStream(stream); + streams[i] = primaryStream == null ? new EmptySampleStream() + : primaryStream.selectEmbeddedTrack(positionUs, embeddedTrackInfo.trackType); + streamResetFlags[i] = true; } - streamResetFlags[i] = true; } } } @@ -178,6 +184,13 @@ import java.util.List; return positionUs; } + @Override + public void discardBuffer(long positionUs) { + for (ChunkSampleStream sampleStream : sampleStreams) { + sampleStream.discardUnselectedEmbeddedTracksTo(positionUs); + } + } + @Override public boolean continueLoading(long positionUs) { return sequenceableLoader.continueLoading(positionUs); @@ -321,6 +334,12 @@ import java.util.List; return new ChunkSampleStream[length]; } + private static void releaseIfEmbeddedSampleStream(SampleStream sampleStream) { + if (sampleStream instanceof EmbeddedSampleStream) { + ((EmbeddedSampleStream) sampleStream).release(); + } + } + private static final class EmbeddedTrackInfo { public final int adaptationSetIndex; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 0ae8becfc0..23ccccbb6b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -189,6 +189,11 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper return positionUs; } + @Override + public void discardBuffer(long positionUs) { + // Do nothing. + } + @Override public boolean continueLoading(long positionUs) { return sequenceableLoader.continueLoading(positionUs); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index b9af9930dc..43cd4a9f8d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -136,6 +136,11 @@ import java.util.ArrayList; return positionUs; } + @Override + public void discardBuffer(long positionUs) { + // Do nothing. + } + @Override public boolean continueLoading(long positionUs) { return sequenceableLoader.continueLoading(positionUs); From 78e7c3c5104b146e7a8ff9ee68c5ea66dcb4287e Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 8 Mar 2017 06:55:02 -0800 Subject: [PATCH 104/140] Make CacheDataSourceFactory createDataSource return specific type ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=149533820 --- .../exoplayer2/upstream/cache/CacheDataSourceFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java index 125bec5fdc..f280cc050b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java @@ -65,7 +65,7 @@ public final class CacheDataSourceFactory implements DataSource.Factory { } @Override - public DataSource createDataSource() { + public CacheDataSource createDataSource() { return new CacheDataSource(cache, upstreamFactory.createDataSource(), cacheReadDataSourceFactory.createDataSource(), cacheWriteDataSinkFactory.createDataSink(), flags, eventListener); From 3be4451e13d604b9a036bd90e8fcefd77c62c5a8 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 8 Mar 2017 07:51:13 -0800 Subject: [PATCH 105/140] Allow injection of DataSource's per type of data This allows the client to define what data source is used for media chunks, encryption chunks and playlists. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=149537766 --- .../hls/DefaultHlsDataSourceFactory.java | 39 +++++++++++++++++++ .../exoplayer2/source/hls/HlsChunkSource.java | 15 ++++--- .../source/hls/HlsDataSourceFactory.java | 35 +++++++++++++++++ .../exoplayer2/source/hls/HlsMediaPeriod.java | 10 ++--- .../exoplayer2/source/hls/HlsMediaSource.java | 9 ++++- .../hls/playlist/HlsPlaylistTracker.java | 12 +++--- .../android/exoplayer2/upstream/DataSpec.java | 4 +- 7 files changed, 105 insertions(+), 19 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/source/hls/HlsDataSourceFactory.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java new file mode 100644 index 0000000000..b90dcb2139 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2017 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.source.hls; + +import com.google.android.exoplayer2.upstream.DataSource; + +/** + * Default implementation of {@link HlsDataSourceFactory}. + */ +public final class DefaultHlsDataSourceFactory implements HlsDataSourceFactory { + + private final DataSource.Factory dataSourceFactory; + + /** + * @param dataSourceFactory The {@link DataSource.Factory} to use for all data types. + */ + public DefaultHlsDataSourceFactory(DataSource.Factory dataSourceFactory) { + this.dataSourceFactory = dataSourceFactory; + } + + @Override + public DataSource createDataSource(int dataType) { + return dataSourceFactory.createDataSource(); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 7ba5cf2df1..5775e4ec38 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -81,7 +81,8 @@ import java.util.Locale; } - private final DataSource dataSource; + private final DataSource mediaDataSource; + private final DataSource encryptionDataSource; private final TimestampAdjusterProvider timestampAdjusterProvider; private final HlsUrl[] variants; private final HlsPlaylistTracker playlistTracker; @@ -105,18 +106,18 @@ import java.util.Locale; /** * @param playlistTracker The {@link HlsPlaylistTracker} from which to obtain media playlists. * @param variants The available variants. - * @param dataSource A {@link DataSource} suitable for loading the media data. + * @param dataSourceFactory An {@link HlsDataSourceFactory} to create {@link DataSource}s for the + * chunks. * @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the * same provider. * @param muxedCaptionFormats List of muxed caption {@link Format}s. */ public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsUrl[] variants, - DataSource dataSource, TimestampAdjusterProvider timestampAdjusterProvider, + HlsDataSourceFactory dataSourceFactory, TimestampAdjusterProvider timestampAdjusterProvider, List muxedCaptionFormats) { this.playlistTracker = playlistTracker; this.variants = variants; - this.dataSource = dataSource; this.timestampAdjusterProvider = timestampAdjusterProvider; this.muxedCaptionFormats = muxedCaptionFormats; Format[] variantFormats = new Format[variants.length]; @@ -125,6 +126,8 @@ import java.util.Locale; variantFormats[i] = variants[i].format; initialTrackSelection[i] = i; } + mediaDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_MEDIA); + encryptionDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_DRM); trackGroup = new TrackGroup(variantFormats); trackSelection = new InitializationTrackSelection(trackGroup, initialTrackSelection); } @@ -285,7 +288,7 @@ import java.util.Locale; Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, null); - out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, selectedUrl, + out.chunk = new HlsMediaChunk(mediaDataSource, dataSpec, initDataSpec, selectedUrl, muxedCaptionFormats, trackSelection.getSelectionReason(), trackSelection.getSelectionData(), startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, discontinuitySequence, isTimestampMaster, timestampAdjuster, previous, encryptionKey, encryptionIv); @@ -341,7 +344,7 @@ import java.util.Locale; private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv, int variantIndex, int trackSelectionReason, Object trackSelectionData) { DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNSET, null, DataSpec.FLAG_ALLOW_GZIP); - return new EncryptionKeyChunk(dataSource, dataSpec, variants[variantIndex].format, + return new EncryptionKeyChunk(encryptionDataSource, dataSpec, variants[variantIndex].format, trackSelectionReason, trackSelectionData, scratchSpace, iv); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsDataSourceFactory.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsDataSourceFactory.java new file mode 100644 index 0000000000..30e7af5a0b --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsDataSourceFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017 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.source.hls; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.DataSource; + +/** + * Creates {@link DataSource}s for HLS playlists, encryption and media chunks. + */ +public interface HlsDataSourceFactory { + + /** + * Creates a {@link DataSource} for the given data type. + * + * @param dataType The data type for which the {@link DataSource} will be used. One of {@link C} + * {@code .DATA_TYPE_*} constants. + * @return A {@link DataSource} for the given data type. + */ + DataSource createDataSource(int dataType); + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 23ccccbb6b..6515e912cd 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -30,7 +30,6 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUr import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; @@ -44,7 +43,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper HlsPlaylistTracker.PlaylistEventListener { private final HlsPlaylistTracker playlistTracker; - private final DataSource.Factory dataSourceFactory; + private final HlsDataSourceFactory dataSourceFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; private final Allocator allocator; @@ -61,7 +60,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; private CompositeSequenceableLoader sequenceableLoader; - public HlsMediaPeriod(HlsPlaylistTracker playlistTracker, DataSource.Factory dataSourceFactory, + public HlsMediaPeriod(HlsPlaylistTracker playlistTracker, HlsDataSourceFactory dataSourceFactory, int minLoadableRetryCount, EventDispatcher eventDispatcher, Allocator allocator, long positionUs) { this.playlistTracker = playlistTracker; @@ -349,9 +348,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants, Format muxedAudioFormat, List muxedCaptionFormats) { - DataSource dataSource = dataSourceFactory.createDataSource(); - HlsChunkSource defaultChunkSource = new HlsChunkSource(playlistTracker, variants, dataSource, - timestampAdjusterProvider, muxedCaptionFormats); + HlsChunkSource defaultChunkSource = new HlsChunkSource(playlistTracker, variants, + dataSourceFactory, timestampAdjusterProvider, muxedCaptionFormats); return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, preparePositionUs, muxedAudioFormat, minLoadableRetryCount, eventDispatcher); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 10e12f0ec6..11de1adfb1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -44,7 +44,7 @@ public final class HlsMediaSource implements MediaSource, public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; private final Uri manifestUri; - private final DataSource.Factory dataSourceFactory; + private final HlsDataSourceFactory dataSourceFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; @@ -60,6 +60,13 @@ public final class HlsMediaSource implements MediaSource, public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { + this(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory), minLoadableRetryCount, + eventHandler, eventListener); + } + + public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, + int minLoadableRetryCount, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index e2e5870777..447d9ba54e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil; +import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; import com.google.android.exoplayer2.upstream.DataSource; @@ -81,7 +82,7 @@ public final class HlsPlaylistTracker implements Loader.Callback playlistBundles; @@ -105,7 +106,7 @@ public final class HlsPlaylistTracker implements Loader.Callback masterPlaylistLoadable = new ParsingLoadable<>( - dataSourceFactory.createDataSource(), initialPlaylistUri, C.DATA_TYPE_MANIFEST, - playlistParser); + dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), initialPlaylistUri, + C.DATA_TYPE_MANIFEST, playlistParser); initialPlaylistLoader.startLoading(masterPlaylistLoadable, this, minRetryCount); } @@ -436,7 +437,8 @@ public final class HlsPlaylistTracker implements Loader.Callback(dataSourceFactory.createDataSource(), + mediaPlaylistLoadable = new ParsingLoadable<>( + dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), C.DATA_TYPE_MANIFEST, playlistParser); } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index c9ff19aee6..d3c63b4454 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -47,7 +47,9 @@ public final class DataSpec { */ public static final int FLAG_ALLOW_GZIP = 1 << 0; - /** Permits content to be cached even if its length can not be resolved. */ + /** + * Permits content to be cached even if its length can not be resolved. + */ public static final int FLAG_ALLOW_CACHING_UNKNOWN_LENGTH = 1 << 1; /** From b6773dba051e79f7791c80aff2396e8fa1886188 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 8 Mar 2017 08:57:26 -0800 Subject: [PATCH 106/140] Add PriorityDataSourceFactory ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=149543747 --- .../upstream/PriorityDataSourceFactory.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 library/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSourceFactory.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSourceFactory.java b/library/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSourceFactory.java new file mode 100644 index 0000000000..daad41a9a6 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSourceFactory.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2017 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.upstream; + +import com.google.android.exoplayer2.upstream.DataSource.Factory; +import com.google.android.exoplayer2.util.PriorityTaskManager; + +/** + * A {@link DataSource.Factory} that produces {@link PriorityDataSource} instances. + */ +public final class PriorityDataSourceFactory implements Factory { + + private final Factory upstreamFactory; + private final PriorityTaskManager priorityTaskManager; + private final int priority; + + /** + * @param upstreamFactory A {@link DataSource.Factory} to be used to create an upstream {@link + * DataSource} for {@link PriorityDataSource}. + * @param priorityTaskManager The priority manager to which PriorityDataSource task is registered. + * @param priority The priority of PriorityDataSource task. + */ + public PriorityDataSourceFactory(Factory upstreamFactory, PriorityTaskManager priorityTaskManager, + int priority) { + this.upstreamFactory = upstreamFactory; + this.priorityTaskManager = priorityTaskManager; + this.priority = priority; + } + + @Override + public PriorityDataSource createDataSource() { + return new PriorityDataSource(upstreamFactory.createDataSource(), priorityTaskManager, + priority); + } + +} From b84c84cc765cd6b2d27e20330735b2d866d310b2 Mon Sep 17 00:00:00 2001 From: cdrolle Date: Thu, 9 Mar 2017 11:18:15 -0800 Subject: [PATCH 107/140] Fixed CEA-708 issue where cues weren't updated at the appropriate times As per the CEA-708-B specification, section 8.10.4, cues don't necessarily need either an ETX command or any of the C1 commands before being updated with the latest buffered content. While those commands do indicate that the cues should be updated immediately, the cues can also be updated after a service block has been processed if it appended text to the buffer. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=149673162 --- .../exoplayer2/text/cea/Cea708Decoder.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java index e04c246ea0..62ffa03bf9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -285,19 +285,26 @@ public final class Cea708Decoder extends CeaDecoder { return; } + // The cues should be updated if we receive a C0 ETX command, any C1 command, or if after + // processing the service block any text has been added to the buffer. See CEA-708-B Section + // 8.10.4 for more details. + boolean cuesNeedUpdate = false; + while (serviceBlockPacket.bitsLeft() > 0) { int command = serviceBlockPacket.readBits(8); if (command != COMMAND_EXT1) { if (command <= GROUP_C0_END) { handleC0Command(command); + // If the C0 command was an ETX command, the cues are updated in handleC0Command. } else if (command <= GROUP_G0_END) { handleG0Character(command); + cuesNeedUpdate = true; } else if (command <= GROUP_C1_END) { handleC1Command(command); - // Cues are always updated after a C1 command - cues = getDisplayCues(); + cuesNeedUpdate = true; } else if (command <= GROUP_G1_END) { handleG1Character(command); + cuesNeedUpdate = true; } else { Log.w(TAG, "Invalid base command: " + command); } @@ -308,15 +315,21 @@ public final class Cea708Decoder extends CeaDecoder { handleC2Command(command); } else if (command <= GROUP_G2_END) { handleG2Character(command); + cuesNeedUpdate = true; } else if (command <= GROUP_C3_END) { handleC3Command(command); } else if (command <= GROUP_G3_END) { handleG3Character(command); + cuesNeedUpdate = true; } else { Log.w(TAG, "Invalid extended command: " + command); } } } + + if (cuesNeedUpdate) { + cues = getDisplayCues(); + } } private void handleC0Command(int command) { From 0cb9802e1bc16ec73bf63c0aacaad9bf6ef050b1 Mon Sep 17 00:00:00 2001 From: cdrolle Date: Thu, 9 Mar 2017 12:10:30 -0800 Subject: [PATCH 108/140] Fixed CEA-708 issues Caption characters weren't being assigned to the correct window and the lack of pen location support was causing multiple lines (and words) to be concatenated. As per the CEA-708-B specification, section 8.10.5, when we encounter a DefineWindow command, we're also supposed to update the current window to the newly defined one. We were not doing this previously, resulting in text that should have been in separate windows being combined into one. Furthermore, some content uses the SetPenLocation command to move the cursor down a line instead of appending a new line. As we don't currently support SetPenLocation, this resulted in multiple lines (and words) being concatenated together, potentially causing the text to extend past the edge of the window/screen. This change implements a workaround (until SetPenLocation is properly supported) for this issue in which setting the pen location to a new row will append a new-line to that window. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=149679613 --- .../exoplayer2/text/cea/Cea708Decoder.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java index 62ffa03bf9..740fd17013 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -470,6 +470,11 @@ public final class Cea708Decoder extends CeaDecoder { case COMMAND_DF7: window = (command - COMMAND_DF0); handleDefineWindow(window); + // We also set the current window to the newly defined window. + if (currentWindow != window) { + currentWindow = window; + currentCueBuilder = cueBuilders[window]; + } break; default: Log.w(TAG, "Invalid C1 command: " + command); @@ -871,6 +876,7 @@ public final class Cea708Decoder extends CeaDecoder { private int foregroundColor; private int backgroundColorStartPosition; private int backgroundColor; + private int row; public CueBuilder() { rolledUpCaptions = new LinkedList<>(); @@ -910,6 +916,7 @@ public final class Cea708Decoder extends CeaDecoder { underlineStartPosition = C.POSITION_UNSET; foregroundColorStartPosition = C.POSITION_UNSET; backgroundColorStartPosition = C.POSITION_UNSET; + row = 0; } public boolean isDefined() { @@ -1044,7 +1051,16 @@ public final class Cea708Decoder extends CeaDecoder { } public void setPenLocation(int row, int column) { - // TODO: Support moving the pen location with a window. + // TODO: Support moving the pen location with a window properly. + + // Until we support proper pen locations, if we encounter a row that's different from the + // previous one, we should append a new line. Otherwise, we'll see strings that should be + // on new lines concatenated with the previous, resulting in 2 words being combined, as + // well as potentially drawing beyond the width of the window/screen. + if (this.row != row) { + append('\n'); + } + this.row = row; } public void backspace() { From cb0187959cd23b138d923aee106b3395163a72af Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 10 Mar 2017 05:32:52 -0800 Subject: [PATCH 109/140] Fix NPE in HLS playback of non-muxed streams ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=149749504 --- .../google/android/exoplayer2/source/hls/HlsMediaPeriod.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 6515e912cd..3a833f5468 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.IdentityHashMap; import java.util.List; @@ -331,7 +332,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper // Build audio stream wrappers. for (int i = 0; i < audioRenditions.size(); i++) { sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO, - new HlsUrl[] {audioRenditions.get(i)}, null, null); + new HlsUrl[] {audioRenditions.get(i)}, null, Collections.emptyList()); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrapper.continuePreparing(); } @@ -340,7 +341,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper for (int i = 0; i < subtitleRenditions.size(); i++) { HlsUrl url = subtitleRenditions.get(i); sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_TEXT, new HlsUrl[] {url}, null, - null); + Collections.emptyList()); sampleStreamWrapper.prepareSingleTrack(url.format); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; } From f21cdcb9c5ea49c603f1ac3189604ff1cb0af1c7 Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 10 Mar 2017 05:54:25 -0800 Subject: [PATCH 110/140] Allow null DataSink.Factory in CacheDataSourceFactory CacheDataSource allows null DataSink. Do the same in CacheDataSourceFactory. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=149750696 --- .../exoplayer2/upstream/cache/CacheDataSourceFactory.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java index f280cc050b..b6fa3b4e2c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java @@ -68,7 +68,8 @@ public final class CacheDataSourceFactory implements DataSource.Factory { public CacheDataSource createDataSource() { return new CacheDataSource(cache, upstreamFactory.createDataSource(), cacheReadDataSourceFactory.createDataSource(), - cacheWriteDataSinkFactory.createDataSink(), flags, eventListener); + cacheWriteDataSinkFactory != null ? cacheWriteDataSinkFactory.createDataSink() : null, + flags, eventListener); } } From 578b9545f0c03483db51d253b04af89a5dcb79e9 Mon Sep 17 00:00:00 2001 From: Ben Wilber Date: Thu, 9 Mar 2017 19:46:24 -0500 Subject: [PATCH 111/140] Support commas in ISO-8601 date/time format for millis --- .../java/com/google/android/exoplayer2/util/UtilTest.java | 2 ++ .../src/main/java/com/google/android/exoplayer2/util/Util.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java index 35e168e514..923d1d8aaa 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java @@ -142,8 +142,10 @@ public class UtilTest extends TestCase { public void testParseXsDateTime() throws Exception { assertEquals(1403219262000L, Util.parseXsDateTime("2014-06-19T23:07:42")); assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00Z")); + assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00,000Z")); assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-08:00")); assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-0800")); + assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55.000-0800")); } public void testUnescapeInvalidFileName() { diff --git a/library/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/src/main/java/com/google/android/exoplayer2/util/Util.java index f4ba21152a..8a32c54356 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -94,7 +94,7 @@ public final class Util { private static final String TAG = "Util"; private static final Pattern XS_DATE_TIME_PATTERN = Pattern.compile( "(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]" - + "(\\d\\d):(\\d\\d):(\\d\\d)(\\.(\\d+))?" + + "(\\d\\d):(\\d\\d):(\\d\\d)([\\.,](\\d+))?" + "([Zz]|((\\+|\\-)(\\d\\d):?(\\d\\d)))?"); private static final Pattern XS_DURATION_PATTERN = Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?" From 15aad266b6cab6c7ec8f69972d90f7ae1ec024fc Mon Sep 17 00:00:00 2001 From: mishragaurav Date: Fri, 10 Mar 2017 09:15:10 -0800 Subject: [PATCH 112/140] Use separate Widevine license keys to package test audio for Exoplayer GTS. Android doesn't support secure decoding for audio. Hence use Audio keys that always require L3 support only. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=149764063 --- .../exoplayer2/playbacktests/gts/DashHostedTest.java | 7 ++++--- .../exoplayer2/playbacktests/gts/DashTestData.java | 12 ++++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java index 24765f282d..1cc220ba0a 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java @@ -178,10 +178,11 @@ public final class DashHostedTest extends ExoHostedTest { private static final float MAX_DROPPED_VIDEO_FRAME_FRACTION = 0.01f; private static final String MANIFEST_URL_PREFIX = "https://storage.googleapis.com/exoplayer-test-" - + "media-1/gen-3/screens/dash-vod-single-segment/"; + + "media-1/gen-4/screens/dash-vod-single-segment/"; - private static final String WIDEVINE_L1_SUFFIX = "-hw.mpd"; - private static final String WIDEVINE_L3_SUFFIX = "-sw.mpd"; + // TODO: Don't need separate suffixes. Clean up. + private static final String WIDEVINE_L1_SUFFIX = ".mpd"; + private static final String WIDEVINE_L3_SUFFIX = ".mpd"; private static final String WIDEVINE_LICENSE_URL = "https://proxy.uat.widevine.com/proxy?provider=widevine_test&video_id="; diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java index c95614bc87..ecb78c6c55 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java @@ -88,10 +88,10 @@ public final class DashTestData { // Widevine encrypted content representation ids. public static final String WIDEVINE_AAC_AUDIO_REPRESENTATION_ID = "0"; - public static final String WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID = "1"; - public static final String WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID = "2"; - public static final String WIDEVINE_H264_MAIN_240P_VIDEO_REPRESENTATION_ID = "3"; - public static final String WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID = "4"; + public static final String WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID = "2"; + public static final String WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID = "3"; + public static final String WIDEVINE_H264_MAIN_240P_VIDEO_REPRESENTATION_ID = "4"; + public static final String WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID = "5"; // The highest quality H264 format mandated by the Android CDD. public static final String WIDEVINE_H264_CDD_FIXED = Util.SDK_INT < 23 ? WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID @@ -113,8 +113,8 @@ public final class DashTestData { public static final String WIDEVINE_H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID = "2"; public static final String WIDEVINE_H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID = "2"; - public static final String WIDEVINE_H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = "1"; - public static final String WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = "2"; + public static final String WIDEVINE_H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = "2"; + public static final String WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = "3"; // The highest quality H265 format mandated by the Android CDD. public static final String WIDEVINE_H265_CDD_FIXED = WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID; From 952bde700b9c8f0a4181f727e165a1633515e72d Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 10 Mar 2017 09:19:54 -0800 Subject: [PATCH 113/140] Ensure only timestamp adjustment masters set first sample timestamps Without this, it is possible that a non timestamp master instances the adjuster with its own chunk start time. When chunks are not aligned, this breaks adjustment. Issue:#2424 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=149764488 --- .../exoplayer2/extractor/ts/TsExtractor.java | 3 +- .../metadata/scte35/SpliceInfoDecoder.java | 3 ++ .../exoplayer2/source/hls/HlsChunkSource.java | 2 +- .../exoplayer2/source/hls/HlsMediaChunk.java | 3 ++ .../source/hls/TimestampAdjusterProvider.java | 5 ++-- .../exoplayer2/util/TimestampAdjuster.java | 30 +++++++++++++++---- 6 files changed, 35 insertions(+), 11 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index ca3ea7ce39..65b97c8a73 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -383,7 +383,8 @@ public final class TsExtractor implements Extractor { if (mode == MODE_SINGLE_PMT || mode == MODE_HLS || remainingPmts == 1) { timestampAdjuster = timestampAdjusters.get(0); } else { - timestampAdjuster = new TimestampAdjuster(timestampAdjusters.get(0).firstSampleTimestampUs); + timestampAdjuster = new TimestampAdjuster( + timestampAdjusters.get(0).getFirstSampleTimestampUs()); timestampAdjusters.add(timestampAdjuster); } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java index 58c23d253a..4050daa1cb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java @@ -88,6 +88,9 @@ public final class SpliceInfoDecoder implements MetadataDecoder { case TYPE_PRIVATE_COMMAND: command = PrivateCommand.parseFromSection(sectionData, spliceCommandLength, ptsAdjustment); break; + default: + // Do nothing. + break; } return command == null ? new Metadata() : new Metadata(command); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 5775e4ec38..ea99dae345 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -282,7 +282,7 @@ import java.util.Locale; int discontinuitySequence = mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence; TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster( - discontinuitySequence, startTimeUs); + discontinuitySequence); // Configure the data source and spec for the chunk. Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 5615db1264..6f516923f9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -242,6 +242,9 @@ import java.util.concurrent.atomic.AtomicInteger; } if (!isMasterTimestampSource) { timestampAdjuster.waitUntilInitialized(); + } 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 { ExtractorInput input = new DefaultExtractorInput(dataSource, diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java index 41fb2c1512..85a4276ea2 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java @@ -36,13 +36,12 @@ public final class TimestampAdjusterProvider { * a chunk with a given discontinuity sequence. * * @param discontinuitySequence The chunk's discontinuity sequence. - * @param startTimeUs The chunk's start time. * @return A {@link TimestampAdjuster}. */ - public TimestampAdjuster getAdjuster(int discontinuitySequence, long startTimeUs) { + public TimestampAdjuster getAdjuster(int discontinuitySequence) { TimestampAdjuster adjuster = timestampAdjusters.get(discontinuitySequence); if (adjuster == null) { - adjuster = new TimestampAdjuster(startTimeUs); + adjuster = new TimestampAdjuster(TimestampAdjuster.DO_NOT_OFFSET); timestampAdjusters.put(discontinuitySequence, adjuster); } return adjuster; diff --git a/library/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java b/library/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java index ace300c6b1..08e2bd0669 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java @@ -34,21 +34,39 @@ public final class TimestampAdjuster { */ private static final long MAX_PTS_PLUS_ONE = 0x200000000L; - public final long firstSampleTimestampUs; - + private long firstSampleTimestampUs; private long timestampOffsetUs; // Volatile to allow isInitialized to be called on a different thread to adjustSampleTimestamp. private volatile long lastSampleTimestamp; /** - * @param firstSampleTimestampUs The desired result of the first call to - * {@link #adjustSampleTimestamp(long)}, or {@link #DO_NOT_OFFSET} if presentation timestamps - * should not be offset. + * @param firstSampleTimestampUs See {@link #setFirstSampleTimestampUs(long)}. */ public TimestampAdjuster(long firstSampleTimestampUs) { - this.firstSampleTimestampUs = firstSampleTimestampUs; lastSampleTimestamp = 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. + * + * @param firstSampleTimestampUs The first adjusted sample timestamp in microseconds, or + * {@link #DO_NOT_OFFSET} if presentation timestamps should not be offset. + */ + public synchronized void setFirstSampleTimestampUs(long firstSampleTimestampUs) { + Assertions.checkState(lastSampleTimestamp == C.TIME_UNSET); + this.firstSampleTimestampUs = firstSampleTimestampUs; + } + + /** + * Returns the first adjusted sample timestamp in microseconds. + * + * @return The first adjusted sample timestamp in microseconds. + */ + public long getFirstSampleTimestampUs() { + return firstSampleTimestampUs; } /** From aede0f894d4060379aa34308a7f8d18ba0050000 Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 10 Mar 2017 11:38:51 -0800 Subject: [PATCH 114/140] Propagate updates of default header fields of the HttpDataSource.BaseFactory to HttpDataSource instances. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=149780233 --- .../ext/cronet/CronetDataSourceTest.java | 3 +- .../ext/cronet/CronetDataSource.java | 54 +++--- .../ext/cronet/CronetDataSourceFactory.java | 6 +- .../ext/okhttp/OkHttpDataSource.java | 34 ++-- .../ext/okhttp/OkHttpDataSourceFactory.java | 7 +- .../upstream/DefaultDataSource.java | 2 +- .../upstream/DefaultHttpDataSource.java | 38 +++-- .../DefaultHttpDataSourceFactory.java | 5 +- .../exoplayer2/upstream/HttpDataSource.java | 155 ++++++++++++++---- 9 files changed, 204 insertions(+), 100 deletions(-) diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index 31def44d36..246e23e172 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -118,7 +118,8 @@ public final class CronetDataSourceTest { TEST_CONNECT_TIMEOUT_MS, TEST_READ_TIMEOUT_MS, true, // resetTimeoutOnRedirects - mockClock)); + mockClock, + null)); when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true); when(mockCronetEngine.newUrlRequestBuilder( anyString(), any(UrlRequest.Callback.class), any(Executor.class))) diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index f6202c6e1e..4f15a6eabc 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -32,7 +32,6 @@ import java.io.IOException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.nio.ByteBuffer; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -98,7 +97,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou private final int connectTimeoutMs; private final int readTimeoutMs; private final boolean resetTimeoutOnRedirects; - private final Map requestProperties; + private final RequestProperties defaultRequestProperties; + private final RequestProperties requestProperties; private final ConditionVariable operation; private final Clock clock; @@ -136,7 +136,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou public CronetDataSource(CronetEngine cronetEngine, Executor executor, Predicate contentTypePredicate, TransferListener listener) { this(cronetEngine, executor, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS, - DEFAULT_READ_TIMEOUT_MILLIS, false); + DEFAULT_READ_TIMEOUT_MILLIS, false, null); } /** @@ -149,17 +149,20 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou * @param connectTimeoutMs The connection timeout, in milliseconds. * @param readTimeoutMs The read timeout, in milliseconds. * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. + * @param defaultRequestProperties The default request properties to be used. */ public CronetDataSource(CronetEngine cronetEngine, Executor executor, Predicate contentTypePredicate, TransferListener listener, - int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects) { + int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, + RequestProperties defaultRequestProperties) { this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs, - readTimeoutMs, resetTimeoutOnRedirects, new SystemClock()); + readTimeoutMs, resetTimeoutOnRedirects, new SystemClock(), defaultRequestProperties); } /* package */ CronetDataSource(CronetEngine cronetEngine, Executor executor, Predicate contentTypePredicate, TransferListener listener, - int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock) { + int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock, + RequestProperties defaultRequestProperties) { this.cronetEngine = Assertions.checkNotNull(cronetEngine); this.executor = Assertions.checkNotNull(executor); this.contentTypePredicate = contentTypePredicate; @@ -168,7 +171,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou this.readTimeoutMs = readTimeoutMs; this.resetTimeoutOnRedirects = resetTimeoutOnRedirects; this.clock = Assertions.checkNotNull(clock); - requestProperties = new HashMap<>(); + this.defaultRequestProperties = defaultRequestProperties; + requestProperties = new RequestProperties(); operation = new ConditionVariable(); } @@ -176,23 +180,17 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou @Override public void setRequestProperty(String name, String value) { - synchronized (requestProperties) { - requestProperties.put(name, value); - } + requestProperties.set(name, value); } @Override public void clearRequestProperty(String name) { - synchronized (requestProperties) { - requestProperties.remove(name); - } + requestProperties.remove(name); } @Override public void clearAllRequestProperties() { - synchronized (requestProperties) { - requestProperties.clear(); - } + requestProperties.clear(); } @Override @@ -421,16 +419,24 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(dataSpec.uri.toString(), this, executor); // Set the headers. - synchronized (requestProperties) { - if (dataSpec.postBody != null && dataSpec.postBody.length != 0 - && !requestProperties.containsKey(CONTENT_TYPE)) { - throw new OpenException("POST request with non-empty body must set Content-Type", dataSpec, - Status.IDLE); - } - for (Entry headerEntry : requestProperties.entrySet()) { - requestBuilder.addHeader(headerEntry.getKey(), headerEntry.getValue()); + boolean isContentTypeHeaderSet = false; + if (defaultRequestProperties != null) { + for (Entry headerEntry : defaultRequestProperties.getSnapshot().entrySet()) { + String key = headerEntry.getKey(); + isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key); + requestBuilder.addHeader(key, headerEntry.getValue()); } } + Map requestPropertiesSnapshot = requestProperties.getSnapshot(); + for (Entry headerEntry : requestPropertiesSnapshot.entrySet()) { + String key = headerEntry.getKey(); + isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key); + requestBuilder.addHeader(key, headerEntry.getValue()); + } + if (dataSpec.postBody != null && dataSpec.postBody.length != 0 && !isContentTypeHeaderSet) { + throw new OpenException("POST request with non-empty body must set Content-Type", dataSpec, + Status.IDLE); + } // Set the Range header. if (currentDataSpec.position != 0 || currentDataSpec.length != C.LENGTH_UNSET) { StringBuilder rangeValue = new StringBuilder(); diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java index 3df901ce59..db560305a7 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.ext.cronet; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.upstream.TransferListener; @@ -68,9 +69,10 @@ public final class CronetDataSourceFactory extends BaseFactory { } @Override - protected CronetDataSource createDataSourceInternal() { + protected CronetDataSource createDataSourceInternal(HttpDataSource.RequestProperties + defaultRequestProperties) { return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener, - connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects); + connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, null, defaultRequestProperties); } } diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index 90a4728933..47850c0637 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -27,7 +27,6 @@ import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; @@ -51,7 +50,8 @@ public class OkHttpDataSource implements HttpDataSource { private final Predicate contentTypePredicate; private final TransferListener listener; private final CacheControl cacheControl; - private final HashMap requestProperties; + private final RequestProperties defaultRequestProperties; + private final RequestProperties requestProperties; private DataSpec dataSpec; private Response response; @@ -87,7 +87,7 @@ public class OkHttpDataSource implements HttpDataSource { */ public OkHttpDataSource(Call.Factory callFactory, String userAgent, Predicate contentTypePredicate, TransferListener listener) { - this(callFactory, userAgent, contentTypePredicate, listener, null); + this(callFactory, userAgent, contentTypePredicate, listener, null, null); } /** @@ -99,16 +99,19 @@ public class OkHttpDataSource implements HttpDataSource { * {@link #open(DataSpec)}. * @param listener An optional listener. * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header. + * @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to + * the server as HTTP headers on every request. */ public OkHttpDataSource(Call.Factory callFactory, String userAgent, Predicate contentTypePredicate, TransferListener listener, - CacheControl cacheControl) { + CacheControl cacheControl, RequestProperties defaultRequestProperties) { this.callFactory = Assertions.checkNotNull(callFactory); this.userAgent = Assertions.checkNotEmpty(userAgent); this.contentTypePredicate = contentTypePredicate; this.listener = listener; this.cacheControl = cacheControl; - this.requestProperties = new HashMap<>(); + this.defaultRequestProperties = defaultRequestProperties; + this.requestProperties = new RequestProperties(); } @Override @@ -125,24 +128,18 @@ public class OkHttpDataSource implements HttpDataSource { public void setRequestProperty(String name, String value) { Assertions.checkNotNull(name); Assertions.checkNotNull(value); - synchronized (requestProperties) { - requestProperties.put(name, value); - } + requestProperties.set(name, value); } @Override public void clearRequestProperty(String name) { Assertions.checkNotNull(name); - synchronized (requestProperties) { - requestProperties.remove(name); - } + requestProperties.remove(name); } @Override public void clearAllRequestProperties() { - synchronized (requestProperties) { - requestProperties.clear(); - } + requestProperties.clear(); } @Override @@ -268,11 +265,14 @@ public class OkHttpDataSource implements HttpDataSource { if (cacheControl != null) { builder.cacheControl(cacheControl); } - synchronized (requestProperties) { - for (Map.Entry property : requestProperties.entrySet()) { - builder.addHeader(property.getKey(), property.getValue()); + if (defaultRequestProperties != null) { + for (Map.Entry property : defaultRequestProperties.getSnapshot().entrySet()) { + builder.header(property.getKey(), property.getValue()); } } + for (Map.Entry property : requestProperties.getSnapshot().entrySet()) { + builder.header(property.getKey(), property.getValue()); + } if (!(position == 0 && length == C.LENGTH_UNSET)) { String rangeRequest = "bytes=" + position + "-"; if (length != C.LENGTH_UNSET) { diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java index 8cbe295fa4..5228065db1 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.ext.okhttp; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.upstream.TransferListener; @@ -59,8 +60,10 @@ public final class OkHttpDataSourceFactory extends BaseFactory { } @Override - protected OkHttpDataSource createDataSourceInternal() { - return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl); + protected OkHttpDataSource createDataSourceInternal( + HttpDataSource.RequestProperties defaultRequestProperties) { + return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl, + defaultRequestProperties); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java index ae6f1e0691..9d13383a56 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java @@ -81,7 +81,7 @@ public final class DefaultDataSource implements DataSource { boolean allowCrossProtocolRedirects) { this(context, listener, new DefaultHttpDataSource(userAgent, null, listener, connectTimeoutMillis, - readTimeoutMillis, allowCrossProtocolRedirects)); + readTimeoutMillis, allowCrossProtocolRedirects, null)); } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index ca0fda9399..599cdddeb9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -32,7 +32,6 @@ import java.net.HttpURLConnection; import java.net.NoRouteToHostException; import java.net.ProtocolException; import java.net.URL; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; @@ -44,8 +43,8 @@ import java.util.regex.Pattern; *

      * By default this implementation will not follow cross-protocol redirects (i.e. redirects from * HTTP to HTTPS or vice versa). Cross-protocol redirects can be enabled by using the - * {@link #DefaultHttpDataSource(String, Predicate, TransferListener, int, int, boolean)} - * constructor and passing {@code true} as the final argument. + * {@link #DefaultHttpDataSource(String, Predicate, TransferListener, int, int, boolean, + * RequestProperties)} constructor and passing {@code true} as the second last argument. */ public class DefaultHttpDataSource implements HttpDataSource { @@ -70,7 +69,8 @@ public class DefaultHttpDataSource implements HttpDataSource { private final int readTimeoutMillis; private final String userAgent; private final Predicate contentTypePredicate; - private final HashMap requestProperties; + private final RequestProperties defaultRequestProperties; + private final RequestProperties requestProperties; private final TransferListener listener; private DataSpec dataSpec; @@ -121,7 +121,8 @@ public class DefaultHttpDataSource implements HttpDataSource { public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate, TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis) { - this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, false); + this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, false, + null); } /** @@ -137,17 +138,21 @@ public class DefaultHttpDataSource implements HttpDataSource { * as an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value. * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP * to HTTPS and vice versa) are enabled. + * @param defaultRequestProperties The default request properties to be sent to the server as + * HTTP headers or {@code null} if not required. */ public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate, TransferListener listener, int connectTimeoutMillis, - int readTimeoutMillis, boolean allowCrossProtocolRedirects) { + int readTimeoutMillis, boolean allowCrossProtocolRedirects, + RequestProperties defaultRequestProperties) { this.userAgent = Assertions.checkNotEmpty(userAgent); this.contentTypePredicate = contentTypePredicate; this.listener = listener; - this.requestProperties = new HashMap<>(); + this.requestProperties = new RequestProperties(); this.connectTimeoutMillis = connectTimeoutMillis; this.readTimeoutMillis = readTimeoutMillis; this.allowCrossProtocolRedirects = allowCrossProtocolRedirects; + this.defaultRequestProperties = defaultRequestProperties; } @Override @@ -164,24 +169,18 @@ public class DefaultHttpDataSource implements HttpDataSource { public void setRequestProperty(String name, String value) { Assertions.checkNotNull(name); Assertions.checkNotNull(value); - synchronized (requestProperties) { - requestProperties.put(name, value); - } + requestProperties.set(name, value); } @Override public void clearRequestProperty(String name) { Assertions.checkNotNull(name); - synchronized (requestProperties) { - requestProperties.remove(name); - } + requestProperties.remove(name); } @Override public void clearAllRequestProperties() { - synchronized (requestProperties) { - requestProperties.clear(); - } + requestProperties.clear(); } @Override @@ -394,11 +393,14 @@ public class DefaultHttpDataSource implements HttpDataSource { HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(connectTimeoutMillis); connection.setReadTimeout(readTimeoutMillis); - synchronized (requestProperties) { - for (Map.Entry property : requestProperties.entrySet()) { + if (defaultRequestProperties != null) { + for (Map.Entry property : defaultRequestProperties.getSnapshot().entrySet()) { connection.setRequestProperty(property.getKey(), property.getValue()); } } + for (Map.Entry property : requestProperties.getSnapshot().entrySet()) { + connection.setRequestProperty(property.getKey(), property.getValue()); + } if (!(position == 0 && length == C.LENGTH_UNSET)) { String rangeRequest = "bytes=" + position + "-"; if (length != C.LENGTH_UNSET) { diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java index 615eb4df97..7679307c5a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java @@ -76,9 +76,10 @@ public final class DefaultHttpDataSourceFactory extends BaseFactory { } @Override - protected DefaultHttpDataSource createDataSourceInternal() { + protected DefaultHttpDataSource createDataSourceInternal( + HttpDataSource.RequestProperties defaultRequestProperties) { return new DefaultHttpDataSource(userAgent, null, listener, connectTimeoutMillis, - readTimeoutMillis, allowCrossProtocolRedirects); + readTimeoutMillis, allowCrossProtocolRedirects, getDefaultRequestProperties()); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java index 1f88828f28..73572d999f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java @@ -17,12 +17,12 @@ package com.google.android.exoplayer2.upstream; import android.support.annotation.IntDef; import android.text.TextUtils; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Predicate; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -41,84 +41,173 @@ public interface HttpDataSource extends DataSource { HttpDataSource createDataSource(); /** - * Sets a default request header for {@link HttpDataSource} instances subsequently created by - * the factory. Previously created instances are not affected. + * Gets the default request properties used by all {@link HttpDataSource}s created by the + * factory. Changes to the properties will be reflected in any future requests made by + * {@link HttpDataSource}s created by the factory. * + * @return The default request properties of the factory. + */ + RequestProperties getDefaultRequestProperties(); + + /** + * Sets a default request header for {@link HttpDataSource} instances created by the factory. + * + * @deprecated Use {@link #getDefaultRequestProperties} instead. * @param name The name of the header field. * @param value The value of the field. */ + @Deprecated void setDefaultRequestProperty(String name, String value); /** - * Clears a default request header for {@link HttpDataSource} instances subsequently created by - * the factory. Previously created instances are not affected. + * Clears a default request header for {@link HttpDataSource} instances created by the factory. * + * @deprecated Use {@link #getDefaultRequestProperties} instead. * @param name The name of the header field. */ + @Deprecated void clearDefaultRequestProperty(String name); /** - * Clears all default request header for all {@link HttpDataSource} instances subsequently - * created by the factory. Previously created instances are not affected. + * Clears all default request headers for all {@link HttpDataSource} instances created by the + * factory. + * + * @deprecated Use {@link #getDefaultRequestProperties} instead. */ + @Deprecated void clearAllDefaultRequestProperties(); } + /** + * Stores HTTP request properties (aka HTTP headers) and provides methods to modify the headers + * in a thread safe way to avoid the potential of creating snapshots of an inconsistent or + * unintended state. + */ + final class RequestProperties { + + private final Map requestProperties; + private Map requestPropertiesSnapshot; + + public RequestProperties() { + requestProperties = new HashMap<>(); + } + + /** + * Sets the specified property {@code value} for the specified {@code name}. If a property for + * this name previously existed, the old value is replaced by the specified value. + * + * @param name The name of the request property. + * @param value The value of the request property. + */ + public synchronized void set(String name, String value) { + requestPropertiesSnapshot = null; + requestProperties.put(name, value); + } + + /** + * Sets the keys and values contained in the map. If a property previously existed, the old + * value is replaced by the specified value. If a property previously existed and is not in the + * map, the property is left unchanged. + * + * @param properties The request properties. + */ + public synchronized void set(Map properties) { + requestPropertiesSnapshot = null; + requestProperties.putAll(properties); + } + + /** + * Removes all properties previously existing and sets the keys and values of the map. + * + * @param properties The request properties. + */ + public synchronized void clearAndSet(Map properties) { + requestPropertiesSnapshot = null; + requestProperties.clear(); + requestProperties.putAll(properties); + } + + /** + * Removes a request property by name. + * + * @param name The name of the request property to remove. + */ + public synchronized void remove(String name) { + requestPropertiesSnapshot = null; + requestProperties.remove(name); + } + + /** + * Clears all request properties. + */ + public synchronized void clear() { + requestPropertiesSnapshot = null; + requestProperties.clear(); + } + + /** + * Gets a snapshot of the request properties. + * + * @return A snapshot of the request properties. + */ + public synchronized Map getSnapshot() { + if (requestPropertiesSnapshot == null) { + requestPropertiesSnapshot = Collections.unmodifiableMap(new HashMap<>(requestProperties)); + } + return requestPropertiesSnapshot; + } + + } + /** * Base implementation of {@link Factory} that sets default request properties. */ abstract class BaseFactory implements Factory { - private final HashMap requestProperties; + private final RequestProperties defaultRequestProperties; public BaseFactory() { - requestProperties = new HashMap<>(); + defaultRequestProperties = new RequestProperties(); } @Override public final HttpDataSource createDataSource() { - HttpDataSource dataSource = createDataSourceInternal(); - synchronized (requestProperties) { - for (Map.Entry property : requestProperties.entrySet()) { - dataSource.setRequestProperty(property.getKey(), property.getValue()); - } - } - return dataSource; + return createDataSourceInternal(defaultRequestProperties); } + @Override + public RequestProperties getDefaultRequestProperties() { + return defaultRequestProperties; + } + + @Deprecated @Override public final void setDefaultRequestProperty(String name, String value) { - Assertions.checkNotNull(name); - Assertions.checkNotNull(value); - synchronized (requestProperties) { - requestProperties.put(name, value); - } + defaultRequestProperties.set(name, value); } + @Deprecated @Override public final void clearDefaultRequestProperty(String name) { - Assertions.checkNotNull(name); - synchronized (requestProperties) { - requestProperties.remove(name); - } + defaultRequestProperties.remove(name); } + @Deprecated @Override public final void clearAllDefaultRequestProperties() { - synchronized (requestProperties) { - requestProperties.clear(); - } + defaultRequestProperties.clear(); } /** - * Called by {@link #createDataSource()} to create a {@link HttpDataSource} instance without - * default request properties set. Default request properties will be set by - * {@link #createDataSource()} before the instance is returned. + * Called by {@link #createDataSource()} to create a {@link HttpDataSource} instance. * - * @return A {@link HttpDataSource} instance without default request properties set. + * @param defaultRequestProperties The default {@code RequestProperties} to be used by the + * {@link HttpDataSource} instance. + * @return A {@link HttpDataSource} instance. */ - protected abstract HttpDataSource createDataSourceInternal(); + protected abstract HttpDataSource createDataSourceInternal(RequestProperties + defaultRequestProperties); } From 343a7302bd04d3bd99700188144937e74042bbb2 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 Mar 2017 08:08:38 -0700 Subject: [PATCH 115/140] Update gradle version ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=149941369 --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 3f4bab597a..05c58be9ab 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.3' + classpath 'com.android.tools.build:gradle:2.3.0' classpath 'com.novoda:bintray-release:0.3.4' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c41838fae2..8c0a9b91f6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Oct 24 14:40:37 BST 2016 +#Mon Mar 13 11:17:14 GMT 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip From 3131074338568291e5142d88fdd76b4bbd3c94b5 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 Mar 2017 08:21:11 -0700 Subject: [PATCH 116/140] Upgrade dependencies Issue: #2516 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=149942409 --- extensions/gvr/build.gradle | 2 +- extensions/okhttp/build.gradle | 2 +- library/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index 5ee9f45509..278d1c248b 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -26,5 +26,5 @@ android { dependencies { compile project(':library') - compile 'com.google.vr:sdk-audio:1.20.0' + compile 'com.google.vr:sdk-audio:1.30.0' } diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index bbf69c60e4..f1f9956027 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -26,7 +26,7 @@ android { dependencies { compile project(':library') - compile('com.squareup.okhttp3:okhttp:3.4.1') { + compile('com.squareup.okhttp3:okhttp:3.6.0') { exclude group: 'org.json' } } diff --git a/library/build.gradle b/library/build.gradle index 0d4bbd0256..3a821fdc3d 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -47,10 +47,10 @@ android { } dependencies { + compile 'com.android.support:support-annotations:25.2.0' androidTestCompile 'com.google.dexmaker:dexmaker:1.2' androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' androidTestCompile 'org.mockito:mockito-core:1.9.5' - compile 'com.android.support:support-annotations:25.0.1' } android.libraryVariants.all { variant -> From 139252c9d3921d26748aa1e04f9ef525eb3dc468 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 13 Mar 2017 16:25:44 +0000 Subject: [PATCH 117/140] Propagate defaultRequestProperties + make getDefaultRequestProperties final --- .../exoplayer2/upstream/DefaultHttpDataSourceFactory.java | 2 +- .../com/google/android/exoplayer2/upstream/HttpDataSource.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java index 7679307c5a..3b3a5a1c16 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java @@ -79,7 +79,7 @@ public final class DefaultHttpDataSourceFactory extends BaseFactory { protected DefaultHttpDataSource createDataSourceInternal( HttpDataSource.RequestProperties defaultRequestProperties) { return new DefaultHttpDataSource(userAgent, null, listener, connectTimeoutMillis, - readTimeoutMillis, allowCrossProtocolRedirects, getDefaultRequestProperties()); + readTimeoutMillis, allowCrossProtocolRedirects, defaultRequestProperties); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java index 73572d999f..3725fc0052 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java @@ -177,7 +177,7 @@ public interface HttpDataSource extends DataSource { } @Override - public RequestProperties getDefaultRequestProperties() { + public final RequestProperties getDefaultRequestProperties() { return defaultRequestProperties; } From 4b1410bced49b8075d928f07a8b6ee62c54a86dd Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 Mar 2017 14:48:32 -0700 Subject: [PATCH 118/140] Simplify + Fix WV encrypted playback tests ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=149993442 --- .../playbacktests/gts/DashHostedTest.java | 24 ++----- .../playbacktests/gts/DashTest.java | 72 ++++++++++--------- .../playbacktests/gts/DashTestData.java | 44 ++++++------ .../gts/DashWidevineOfflineTest.java | 18 +++-- 4 files changed, 74 insertions(+), 84 deletions(-) diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java index 1cc220ba0a..b82a31f6cd 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java @@ -135,13 +135,12 @@ public final class DashHostedTest extends ExoHostedTest { } public Builder setManifestUrl(String manifestUrl) { - this.manifestUrl = MANIFEST_URL_PREFIX + manifestUrl; + this.manifestUrl = manifestUrl; return this; } - public Builder setManifestUrlForWidevine(String manifestUrl, String videoMimeType) { - this.useL1Widevine = isL1WidevineAvailable(videoMimeType); - this.manifestUrl = getWidevineManifestUrl(manifestUrl, useL1Widevine); + public Builder setWidevineMimeType(String mimeType) { + this.useL1Widevine = isL1WidevineAvailable(mimeType); this.widevineLicenseUrl = getWidevineLicenseUrl(useL1Widevine); return this; } @@ -177,13 +176,6 @@ public final class DashHostedTest extends ExoHostedTest { private static final int MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES = 10; private static final float MAX_DROPPED_VIDEO_FRAME_FRACTION = 0.01f; - private static final String MANIFEST_URL_PREFIX = "https://storage.googleapis.com/exoplayer-test-" - + "media-1/gen-4/screens/dash-vod-single-segment/"; - - // TODO: Don't need separate suffixes. Clean up. - private static final String WIDEVINE_L1_SUFFIX = ".mpd"; - private static final String WIDEVINE_L3_SUFFIX = ".mpd"; - private static final String WIDEVINE_LICENSE_URL = "https://proxy.uat.widevine.com/proxy?provider=widevine_test&video_id="; private static final String WIDEVINE_SW_CRYPTO_CONTENT_ID = "exoplayer_test_1"; @@ -204,11 +196,6 @@ public final class DashHostedTest extends ExoHostedTest { boolean needsCddLimitedRetry; - public static String getWidevineManifestUrl(String manifestUrl, boolean useL1Widevine) { - return MANIFEST_URL_PREFIX + manifestUrl - + (useL1Widevine ? WIDEVINE_L1_SUFFIX : WIDEVINE_L3_SUFFIX); - } - public static String getWidevineLicenseUrl(boolean useL1Widevine) { return WIDEVINE_LICENSE_URL + (useL1Widevine ? WIDEVINE_HW_SECURE_DECODE_CONTENT_ID : WIDEVINE_SW_CRYPTO_CONTENT_ID); @@ -216,13 +203,12 @@ public final class DashHostedTest extends ExoHostedTest { @TargetApi(18) @SuppressWarnings("ResourceType") - public static boolean isL1WidevineAvailable(String videoMimeType) { + public static boolean isL1WidevineAvailable(String mimeType) { try { // Force L3 if secure decoder is not available. - if (MediaCodecUtil.getDecoderInfo(videoMimeType, true) == null) { + if (MediaCodecUtil.getDecoderInfo(mimeType, true) == null) { return false; } - MediaDrm mediaDrm = new MediaDrm(WIDEVINE_UUID); String securityProperty = mediaDrm.getPropertyString(SECURITY_LEVEL_PROPERTY); mediaDrm.release(); diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java index 6ae66f24e1..c41cc1b0b7 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java @@ -214,7 +214,7 @@ public final class DashTest extends ActivityInstrumentationTestCase2 offlineLicenseHelper; private byte[] offlineLicenseKeySetId; @@ -52,18 +51,17 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa super.setUp(); builder = new DashHostedTest.Builder(TAG) .setStreamName("test_widevine_h264_fixed_offline") - .setManifestUrlForWidevine(DashTestData.WIDEVINE_H264_MANIFEST_PREFIX, MimeTypes.VIDEO_H264) + .setManifestUrl(DashTestData.WIDEVINE_H264_MANIFEST) + .setWidevineMimeType(MimeTypes.VIDEO_H264) .setFullPlaybackNoSeeking(true) .setCanIncludeAdditionalVideoFormats(false) .setAudioVideoFormats(DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, DashTestData.WIDEVINE_H264_CDD_FIXED); boolean useL1Widevine = DashHostedTest.isL1WidevineAvailable(MimeTypes.VIDEO_H264); - widevineManifestUrl = DashHostedTest - .getWidevineManifestUrl(DashTestData.WIDEVINE_H264_MANIFEST_PREFIX, useL1Widevine); String widevineLicenseUrl = DashHostedTest.getWidevineLicenseUrl(useL1Widevine); httpDataSourceFactory = new DefaultHttpDataSourceFactory(USER_AGENT); - offlineLicenseHelper = OfflineLicenseHelper.newWidevineInstance(widevineLicenseUrl, + offlineLicenseHelper = OfflineLicenseHelper.newWidevineInstance(widevineLicenseUrl, httpDataSourceFactory); } @@ -125,7 +123,7 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa downloadLicense(); // Wait until the license expires - long licenseDuration = + long licenseDuration = offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId).first; assertTrue("License duration should be less than 30 sec. " + "Server settings might have changed.", licenseDuration < 30); @@ -134,7 +132,7 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa wait(licenseDuration * 1000 + 2000); } long previousDuration = licenseDuration; - licenseDuration = + licenseDuration = offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId).first; assertTrue("License duration should be decreasing.", previousDuration > licenseDuration); } @@ -150,7 +148,7 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa downloadLicense(); // During playback pause until the license expires then continue playback - Pair licenseDurationRemainingSec = + Pair licenseDurationRemainingSec = offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId); long licenseDuration = licenseDurationRemainingSec.first; assertTrue("License duration should be less than 30 sec. " @@ -163,10 +161,10 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa .setActionSchedule(schedule) .runTest(getActivity(), getInstrumentation()); } - + private void downloadLicense() throws InterruptedException, DrmSessionException, IOException { offlineLicenseKeySetId = offlineLicenseHelper.download( - httpDataSourceFactory.createDataSource(), widevineManifestUrl); + httpDataSourceFactory.createDataSource(), DashTestData.WIDEVINE_H264_MANIFEST); Assert.assertNotNull(offlineLicenseKeySetId); Assert.assertTrue(offlineLicenseKeySetId.length > 0); builder.setOfflineLicenseKeySetId(offlineLicenseKeySetId); From 204537ed40e85134a1c5c91f0dd1e5453775043a Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 14 Mar 2017 03:47:26 -0700 Subject: [PATCH 119/140] Pre-modularization cleanup - Use a variable for the (default) minSdkVersion. There will be more modules that need it, and it'll be easier to manage if it's in one place. Issue: #2139 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150050663 --- build.gradle | 8 +++++--- extensions/cronet/build.gradle | 2 +- extensions/ffmpeg/build.gradle | 2 +- extensions/flac/build.gradle | 2 +- extensions/gvr/build.gradle | 1 - extensions/okhttp/build.gradle | 2 +- extensions/opus/build.gradle | 3 +-- extensions/vp9/build.gradle | 2 +- library/build.gradle | 7 +------ playbacktests/build.gradle | 2 +- testutils/build.gradle | 2 +- 11 files changed, 14 insertions(+), 19 deletions(-) diff --git a/build.gradle b/build.gradle index 05c58be9ab..e94849fbf1 100644 --- a/build.gradle +++ b/build.gradle @@ -11,9 +11,6 @@ // 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. - -// Top-level build file where you can add configuration options common to all sub-projects/modules. - buildscript { repositories { jcenter() @@ -29,6 +26,11 @@ allprojects { jcenter() } project.ext { + // Important: ExoPlayer specifies a minSdkVersion of 9 because various + // components provided by the library may be of use on older devices. + // However, please note that the core media playback functionality + // provided by the library requires API level 16 or greater. + minSdkVersion=9 compileSdkVersion=25 targetSdkVersion=25 buildToolsVersion='25' diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index a245133937..f031a9dc48 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -18,7 +18,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion 9 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle index e0f6d900a0..a6523788cb 100644 --- a/extensions/ffmpeg/build.gradle +++ b/extensions/ffmpeg/build.gradle @@ -18,7 +18,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion 9 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion consumerProguardFiles 'proguard-rules.txt' } diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 7f1a790dad..1c23b9987c 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -18,7 +18,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion 9 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion consumerProguardFiles 'proguard-rules.txt' } diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index 278d1c248b..320397656e 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -18,7 +18,6 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - // Required by com.google.vr:sdk-audio. minSdkVersion 19 targetSdkVersion project.ext.targetSdkVersion } diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index f1f9956027..f4cdfdb853 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -19,7 +19,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion 9 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion } } diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle index d354654c14..a6523788cb 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -18,7 +18,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion 9 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion consumerProguardFiles 'proguard-rules.txt' } @@ -32,4 +32,3 @@ android { dependencies { compile project(':library') } - diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index d354654c14..91d80f4970 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -18,7 +18,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion 9 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion consumerProguardFiles 'proguard-rules.txt' } diff --git a/library/build.gradle b/library/build.gradle index 3a821fdc3d..0ad54aadb2 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -21,12 +21,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - // Important: ExoPlayerLib specifies a minSdkVersion of 9 because - // various components provided by the library may be of use on older - // devices. However, please note that the core video playback - // functionality provided by the library requires API level 16 or - // greater. - minSdkVersion 9 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion consumerProguardFiles 'proguard-rules.txt' } diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle index c53793b534..cb82d0a466 100644 --- a/playbacktests/build.gradle +++ b/playbacktests/build.gradle @@ -18,7 +18,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion 9 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion } } diff --git a/testutils/build.gradle b/testutils/build.gradle index 83ff065f9a..a97c743384 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -18,7 +18,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion 9 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion } } From 7c587c6b8290966eeb6893eaf98bc044bb545578 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 14 Mar 2017 05:07:11 -0700 Subject: [PATCH 120/140] Prevent playlist loading if refresh is already scheduled This greatly reduces the amount of server requests issued by the playlist tracker. Issue:#2548 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150055046 --- .../exoplayer2/source/hls/playlist/HlsPlaylistTracker.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index 447d9ba54e..311f279b96 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -432,6 +432,7 @@ public final class HlsPlaylistTracker implements Loader.Callback Date: Tue, 14 Mar 2017 05:27:03 -0700 Subject: [PATCH 121/140] No-op fix for playback tests super.onQueueInputBuffer is no longer a no-op in all configurations. It doesn't make any difference in practice for these tests, but for completeness we should call up. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150056224 --- .../exoplayer2/playbacktests/util/DebugSimpleExoPlayer.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugSimpleExoPlayer.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugSimpleExoPlayer.java index ede172ad29..c530ab63c1 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugSimpleExoPlayer.java +++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugSimpleExoPlayer.java @@ -99,12 +99,14 @@ public class DebugSimpleExoPlayer extends SimpleExoPlayer { @Override protected void onQueueInputBuffer(DecoderInputBuffer buffer) { + super.onQueueInputBuffer(buffer); insertTimestamp(buffer.timeUs); maybeShiftTimestampsList(); } @Override protected void onProcessedOutputBuffer(long presentationTimeUs) { + super.onProcessedOutputBuffer(presentationTimeUs); bufferCount++; long expectedTimestampUs = dequeueTimestamp(); if (expectedTimestampUs != presentationTimeUs) { From 8a411c310dfdaf37d03cd9ed612af6848175af46 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 14 Mar 2017 09:51:13 -0700 Subject: [PATCH 122/140] Suppress some lint errors ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150078703 --- demo/build.gradle | 5 +++++ .../android/exoplayer2/demo/SampleChooserActivity.java | 2 +- extensions/okhttp/build.gradle | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/demo/build.gradle b/demo/build.gradle index 007dc70590..01946c8504 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -33,6 +33,11 @@ android { } } + lintOptions { + // The demo app does not have translations. + disable 'MissingTranslation' + } + productFlavors { noExtensions withExtensions diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index d6655b79b9..081ad190b5 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -262,7 +262,7 @@ public class SampleChooserActivity extends Activity { } private UUID getDrmUuid(String typeString) throws ParserException { - switch (typeString.toLowerCase()) { + switch (Util.toLowerInvariant(typeString)) { case "widevine": return C.WIDEVINE_UUID; case "playready": diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index f4cdfdb853..8fc4d08ae3 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -22,6 +22,11 @@ android { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion } + + lintOptions { + // See: https://github.com/square/okio/issues/58 + warning 'InvalidPackage' + } } dependencies { From 70926057c5a5006b835f7c8921b649013cde2e43 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 14 Mar 2017 10:12:28 -0700 Subject: [PATCH 123/140] Fix some more incorrect playback test stream IDs ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150080957 --- .../android/exoplayer2/playbacktests/gts/DashTestData.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java index 4be2bc02bc..27a57f76b4 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java @@ -111,9 +111,9 @@ public final class DashTestData { WIDEVINE_H264_MAIN_240P_VIDEO_REPRESENTATION_ID, WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID}; - public static final String WIDEVINE_H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID = "2"; - public static final String WIDEVINE_H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID = "2"; - public static final String WIDEVINE_H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID = "2"; + public static final String WIDEVINE_H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID = "3"; + public static final String WIDEVINE_H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID = "3"; + public static final String WIDEVINE_H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID = "3"; public static final String WIDEVINE_H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = "2"; public static final String WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = "3"; From d077e23daa730c8d5a85ce84fb301a41de439f1e Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 Mar 2017 06:24:12 -0700 Subject: [PATCH 124/140] Improve DefaultExtractorInput's peek buffer sizing. - Don't resize the peek buffer to be twice as large as a large amount! - Trim the peek buffer, to allow large peek buffer allocations to be reclaimed. Issue: #2553 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150184291 --- .../extractor/DefaultExtractorInput.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java b/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java index bca5ecf3bd..87355a6c78 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; import java.util.Arrays; @@ -27,6 +28,8 @@ import java.util.Arrays; */ public final class DefaultExtractorInput implements ExtractorInput { + private static final int PEEK_MIN_FREE_SPACE_AFTER_RESIZE = 64 * 1024; + private static final int PEEK_MAX_FREE_SPACE = 512 * 1024; private static final byte[] SCRATCH_SPACE = new byte[4096]; private final DataSource dataSource; @@ -46,7 +49,7 @@ public final class DefaultExtractorInput implements ExtractorInput { this.dataSource = dataSource; this.position = position; this.streamLength = length; - peekBuffer = new byte[8 * 1024]; + peekBuffer = new byte[PEEK_MIN_FREE_SPACE_AFTER_RESIZE]; } @Override @@ -176,7 +179,9 @@ public final class DefaultExtractorInput implements ExtractorInput { private void ensureSpaceForPeek(int length) { int requiredLength = peekBufferPosition + length; if (requiredLength > peekBuffer.length) { - peekBuffer = Arrays.copyOf(peekBuffer, Math.max(peekBuffer.length * 2, requiredLength)); + int newPeekCapacity = Util.constrainValue(peekBuffer.length * 2, + requiredLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE, requiredLength + PEEK_MAX_FREE_SPACE); + peekBuffer = Arrays.copyOf(peekBuffer, newPeekCapacity); } } @@ -218,7 +223,12 @@ public final class DefaultExtractorInput implements ExtractorInput { private void updatePeekBuffer(int bytesConsumed) { peekBufferLength -= bytesConsumed; peekBufferPosition = 0; - System.arraycopy(peekBuffer, bytesConsumed, peekBuffer, 0, peekBufferLength); + byte[] newPeekBuffer = peekBuffer; + if (peekBufferLength < peekBuffer.length - PEEK_MAX_FREE_SPACE) { + newPeekBuffer = new byte[peekBufferLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE]; + } + System.arraycopy(peekBuffer, bytesConsumed, newPeekBuffer, 0, peekBufferLength); + peekBuffer = newPeekBuffer; } /** From db5f81ecfd9310adf4daf0875662be4dcc3f7652 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 Mar 2017 06:32:47 -0700 Subject: [PATCH 125/140] Allow disabling ID3 metadata parsing if not required Issue: #2553 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150184824 --- .../extractor/GaplessInfoHolder.java | 13 ++++ .../extractor/mp3/Mp3Extractor.java | 16 ++++- .../exoplayer2/metadata/id3/Id3Decoder.java | 59 +++++++++++++++---- 3 files changed, 75 insertions(+), 13 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java index 7e2a1b4a23..75d8b4cf2d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.CommentFrame; +import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -26,6 +27,18 @@ import java.util.regex.Pattern; */ public final class GaplessInfoHolder { + /** + * A {@link FramePredicate} suitable for use when decoding {@link Metadata} that will be passed + * to {@link #setFromMetadata(Metadata)}. Only frames that might contain gapless playback + * information are decoded. + */ + public static final FramePredicate GAPLESS_INFO_ID3_FRAME_PREDICATE = new FramePredicate() { + @Override + public boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3) { + return id0 == 'C' && id1 == 'O' && id2 == 'M' && (id3 == 'M' || majorVersion == 2); + } + }; + private static final String GAPLESS_COMMENT_ID = "iTunSMPB"; private static final Pattern GAPLESS_COMMENT_PATTERN = Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})"); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 00394f7912..b0faad71c0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -58,13 +58,18 @@ public final class Mp3Extractor implements Extractor { * Flags controlling the behavior of the extractor. */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}) + @IntDef(flag = true, value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, FLAG_DISABLE_ID3_METADATA}) public @interface Flags {} /** * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would * otherwise not be possible. */ public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; + /** + * Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not + * required. + */ + public static final int FLAG_DISABLE_ID3_METADATA = 2; /** * The maximum number of bytes to search when synchronizing, before giving up. @@ -178,7 +183,8 @@ public final class Mp3Extractor implements Extractor { trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay, - gaplessInfoHolder.encoderPadding, null, null, 0, null, metadata)); + gaplessInfoHolder.encoderPadding, null, null, 0, null, + (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata)); } return readSample(input); } @@ -311,7 +317,11 @@ public final class Mp3Extractor implements Extractor { byte[] id3Data = new byte[tagLength]; System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH); input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength); - metadata = new Id3Decoder().decode(id3Data, tagLength); + // We need to parse enough ID3 metadata to retrieve any gapless playback information even + // if ID3 metadata parsing is disabled. + Id3Decoder.FramePredicate id3FramePredicate = (flags & FLAG_DISABLE_ID3_METADATA) != 0 + ? GaplessInfoHolder.GAPLESS_INFO_ID3_FRAME_PREDICATE : null; + metadata = new Id3Decoder(id3FramePredicate).decode(id3Data, tagLength); if (metadata != null) { gaplessInfoHolder.setFromMetadata(metadata); } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index d5f5b08370..cbe6c65030 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -34,6 +34,25 @@ import java.util.Locale; */ public final class Id3Decoder implements MetadataDecoder { + /** + * A predicate for determining whether individual frames should be decoded. + */ + public interface FramePredicate { + + /** + * Returns whether a frame with the specified parameters should be decoded. + * + * @param majorVersion The major version of the ID3 tag. + * @param id0 The first byte of the frame ID. + * @param id1 The second byte of the frame ID. + * @param id2 The third byte of the frame ID. + * @param id3 The fourth byte of the frame ID. + * @return Whether the frame should be decoded. + */ + boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3); + + } + private static final String TAG = "Id3Decoder"; /** @@ -50,6 +69,19 @@ public final class Id3Decoder implements MetadataDecoder { private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; private static final int ID3_TEXT_ENCODING_UTF_8 = 3; + private final FramePredicate framePredicate; + + public Id3Decoder() { + this(null); + } + + /** + * @param framePredicate Determines which frames are decoded. May be null to decode all frames. + */ + public Id3Decoder(FramePredicate framePredicate) { + this.framePredicate = framePredicate; + } + @Override public Metadata decode(MetadataInputBuffer inputBuffer) { ByteBuffer buffer = inputBuffer.data; @@ -94,7 +126,7 @@ public final class Id3Decoder implements MetadataDecoder { int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10; while (id3Data.bytesLeft() >= frameHeaderSize) { Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack, - frameHeaderSize); + frameHeaderSize, framePredicate); if (frame != null) { id3Frames.add(frame); } @@ -200,7 +232,7 @@ public final class Id3Decoder implements MetadataDecoder { } private static Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data, - boolean unsignedIntFrameSizeHack, int frameHeaderSize) { + boolean unsignedIntFrameSizeHack, int frameHeaderSize, FramePredicate framePredicate) { int frameId0 = id3Data.readUnsignedByte(); int frameId1 = id3Data.readUnsignedByte(); int frameId2 = id3Data.readUnsignedByte(); @@ -234,6 +266,13 @@ public final class Id3Decoder implements MetadataDecoder { return null; } + if (framePredicate != null + && !framePredicate.evaluate(majorVersion, frameId0, frameId1, frameId2, frameId3)) { + // Filtered by the predicate. + id3Data.setPosition(nextFramePosition); + return null; + } + // Frame flags. boolean isCompressed = false; boolean isEncrypted = false; @@ -302,10 +341,10 @@ public final class Id3Decoder implements MetadataDecoder { frame = decodeCommentFrame(id3Data, frameSize); } else if (frameId0 == 'C' && frameId1 == 'H' && frameId2 == 'A' && frameId3 == 'P') { frame = decodeChapterFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack, - frameHeaderSize); + frameHeaderSize, framePredicate); } else if (frameId0 == 'C' && frameId1 == 'T' && frameId2 == 'O' && frameId3 == 'C') { frame = decodeChapterTOCFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack, - frameHeaderSize); + frameHeaderSize, framePredicate); } else { String id = majorVersion == 2 ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2) @@ -513,8 +552,8 @@ public final class Id3Decoder implements MetadataDecoder { } private static ChapterFrame decodeChapterFrame(ParsableByteArray id3Data, int frameSize, - int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize) - throws UnsupportedEncodingException { + int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize, + FramePredicate framePredicate) throws UnsupportedEncodingException { int framePosition = id3Data.getPosition(); int chapterIdEndIndex = indexOfZeroByte(id3Data.data, framePosition); String chapterId = new String(id3Data.data, framePosition, chapterIdEndIndex - framePosition, @@ -536,7 +575,7 @@ public final class Id3Decoder implements MetadataDecoder { int limit = framePosition + frameSize; while (id3Data.getPosition() < limit) { Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack, - frameHeaderSize); + frameHeaderSize, framePredicate); if (frame != null) { subFrames.add(frame); } @@ -548,8 +587,8 @@ public final class Id3Decoder implements MetadataDecoder { } private static ChapterTocFrame decodeChapterTOCFrame(ParsableByteArray id3Data, int frameSize, - int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize) - throws UnsupportedEncodingException { + int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize, + FramePredicate framePredicate) throws UnsupportedEncodingException { int framePosition = id3Data.getPosition(); int elementIdEndIndex = indexOfZeroByte(id3Data.data, framePosition); String elementId = new String(id3Data.data, framePosition, elementIdEndIndex - framePosition, @@ -573,7 +612,7 @@ public final class Id3Decoder implements MetadataDecoder { int limit = framePosition + frameSize; while (id3Data.getPosition() < limit) { Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack, - frameHeaderSize); + frameHeaderSize, framePredicate); if (frame != null) { subFrames.add(frame); } From 7c5f0b7d3b39ef2b1776e4bdb588289750580a31 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 15 Mar 2017 06:37:02 -0700 Subject: [PATCH 126/140] Make Video track selections before others This will allow us to make a single adaptive selection prioritizing video selections. Issue:#1975 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150185086 --- .../trackselection/DefaultTrackSelector.java | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index f72f99f212..46e2346628 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -421,18 +421,26 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) throws ExoPlaybackException { // Make a track selection for each renderer. - TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCapabilities.length]; + int rendererCount = rendererCapabilities.length; + TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCount]; Parameters params = paramsReference.get(); - for (int i = 0; i < rendererCapabilities.length; i++) { + + for (int i = 0; i < rendererCount; i++) { + if (C.TRACK_TYPE_VIDEO == rendererCapabilities[i].getTrackType()) { + rendererTrackSelections[i] = selectVideoTrack(rendererCapabilities[i], + rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth, + params.maxVideoHeight, params.maxVideoBitrate, params.allowNonSeamlessAdaptiveness, + params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight, + params.orientationMayChange, adaptiveVideoTrackSelectionFactory, + params.exceedVideoConstraintsIfNecessary, + params.exceedRendererCapabilitiesIfNecessary); + } + } + + for (int i = 0; i < rendererCount; i++) { switch (rendererCapabilities[i].getTrackType()) { case C.TRACK_TYPE_VIDEO: - rendererTrackSelections[i] = selectVideoTrack(rendererCapabilities[i], - rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth, - params.maxVideoHeight, params.maxVideoBitrate, params.allowNonSeamlessAdaptiveness, - params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight, - params.orientationMayChange, adaptiveVideoTrackSelectionFactory, - params.exceedVideoConstraintsIfNecessary, - params.exceedRendererCapabilitiesIfNecessary); + // Already done. Do nothing. break; case C.TRACK_TYPE_AUDIO: rendererTrackSelections[i] = selectAudioTrack(rendererTrackGroupArrays[i], From a9617af29c2c760c8e439d390ef02f0b4e055a6b Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 Mar 2017 06:44:11 -0700 Subject: [PATCH 127/140] Use fast surface switching on API level 23+ when possible ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150185483 --- .../video/MediaCodecVideoRenderer.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 280f004211..059628e0c8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -298,13 +298,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } private void setSurface(Surface surface) throws ExoPlaybackException { - // We only need to release and reinitialize the codec if the surface has changed. + // We only need to update the codec if the surface has changed. if (this.surface != surface) { this.surface = surface; int state = getState(); if (state == STATE_ENABLED || state == STATE_STARTED) { - releaseCodec(); - maybeInitCodec(); + MediaCodec codec = getCodec(); + if (Util.SDK_INT >= 23 && codec != null && surface != null) { + setOutputSurfaceV23(codec, surface); + } else { + releaseCodec(); + maybeInitCodec(); + } } } // Clear state so that we always call the event listener with the video size and when a frame @@ -589,6 +594,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return frameworkMediaFormat; } + @TargetApi(23) + private static void setOutputSurfaceV23(MediaCodec codec, Surface surface) { + codec.setOutputSurface(surface); + } + @TargetApi(21) private static void configureTunnelingV21(MediaFormat mediaFormat, int tunnelingAudioSessionId) { mediaFormat.setFeatureEnabled(CodecCapabilities.FEATURE_TunneledPlayback, true); From d6db5066cda2872afe9b6c0bd6f2906b01a176ec Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 Mar 2017 06:47:35 -0700 Subject: [PATCH 128/140] Improve publishing to Bintray - Update bintray-release version - Publish to exoplayer-test unless -PpublicRepo=true - Publish GVR extension - Minimize duplication with new publish.gradle ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150185740 --- build.gradle | 10 ++++++++-- extensions/gvr/build.gradle | 6 ++++++ extensions/okhttp/build.gradle | 13 ++++--------- library/build.gradle | 13 ++++--------- publish.gradle | 24 ++++++++++++++++++++++++ 5 files changed, 46 insertions(+), 20 deletions(-) create mode 100644 publish.gradle diff --git a/build.gradle b/build.gradle index e94849fbf1..fabac36293 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:2.3.0' - classpath 'com.novoda:bintray-release:0.3.4' + classpath 'com.novoda:bintray-release:0.4.0' } } @@ -34,10 +34,16 @@ allprojects { compileSdkVersion=25 targetSdkVersion=25 buildToolsVersion='25' - releaseRepoName = 'exoplayer' + releaseRepoName = getBintrayRepo() releaseUserOrg = 'google' releaseGroupId = 'com.google.android.exoplayer' releaseVersion = 'r2.2.0' releaseWebsite = 'https://github.com/google/ExoPlayer' } } + +def getBintrayRepo() { + boolean publicRepo = hasProperty('publicRepo') && + property('publicRepo').toBoolean() + return publicRepo ? 'exoplayer' : 'exoplayer-test' +} diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index 320397656e..5156cf0540 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -27,3 +27,9 @@ dependencies { compile project(':library') compile 'com.google.vr:sdk-audio:1.30.0' } + +ext { + releaseArtifact = 'extension-gvr' + releaseDescription = 'Google VR extension for ExoPlayer.' +} +apply from: '../../publish.gradle' diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index 8fc4d08ae3..3a2daefb8f 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. apply plugin: 'com.android.library' -apply plugin: 'bintray-release' android { compileSdkVersion project.ext.compileSdkVersion @@ -36,12 +35,8 @@ dependencies { } } -publish { - artifactId = 'extension-okhttp' - description = 'An OkHttp extension for ExoPlayer.' - repoName = releaseRepoName - userOrg = releaseUserOrg - groupId = releaseGroupId - version = releaseVersion - website = releaseWebsite +ext { + releaseArtifact = 'extension-okhttp' + releaseDescription = 'OkHttp extension for ExoPlayer.' } +apply from: '../../publish.gradle' diff --git a/library/build.gradle b/library/build.gradle index 0ad54aadb2..abca404cfa 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -14,7 +14,6 @@ import com.android.builder.core.BuilderConstants apply plugin: 'com.android.library' -apply plugin: 'bintray-release' android { compileSdkVersion project.ext.compileSdkVersion @@ -81,12 +80,8 @@ android.libraryVariants.all { variant -> } } -publish { - artifactId = 'exoplayer' - description = 'The ExoPlayer library.' - repoName = releaseRepoName - userOrg = releaseUserOrg - groupId = releaseGroupId - version = releaseVersion - website = releaseWebsite +ext { + releaseArtifact = 'exoplayer' + releaseDescription = 'The ExoPlayer library.' } +apply from: '../publish.gradle' diff --git a/publish.gradle b/publish.gradle new file mode 100644 index 0000000000..17214959ab --- /dev/null +++ b/publish.gradle @@ -0,0 +1,24 @@ +// Copyright (C) 2017 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. +apply plugin: 'bintray-release' + +publish { + artifactId = releaseArtifact + description = releaseDescription + repoName = releaseRepoName + userOrg = releaseUserOrg + groupId = releaseGroupId + version = releaseVersion + website = releaseWebsite +} From f092c4446f5f2d2c81a904070ab773857e99fa88 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 15 Mar 2017 07:49:51 -0700 Subject: [PATCH 129/140] Move TestUtil.createTempFolder and TestUtil.recursiveDelete to Util class ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150190520 --- .../upstream/cache/CacheDataSourceTest.java | 6 +++--- .../cache/CachedContentIndexTest.java | 6 +++--- .../cache/CachedRegionTrackerTest.java | 5 +++-- .../upstream/cache/SimpleCacheSpanTest.java | 6 +++--- .../upstream/cache/SimpleCacheTest.java | 5 ++--- .../exoplayer2/util/AtomicFileTest.java | 5 ++--- .../google/android/exoplayer2/util/Util.java | 19 +++++++++++++++++++ .../android/exoplayer2/testutil/TestUtil.java | 19 ------------------- 8 files changed, 35 insertions(+), 36 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index a5b272cebd..6689d73ff1 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -20,9 +20,9 @@ import android.test.InstrumentationTestCase; import android.test.MoreAsserts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.FakeDataSource; -import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.FileDataSource; +import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; import java.util.Arrays; @@ -42,13 +42,13 @@ public class CacheDataSourceTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); + cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); } @Override protected void tearDown() throws Exception { - TestUtil.recursiveDelete(cacheDir); + Util.recursiveDelete(cacheDir); } public void testMaxCacheFileSize() throws Exception { diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java index 4fbcc92e3d..7f6e203c20 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java @@ -4,7 +4,7 @@ import android.test.InstrumentationTestCase; import android.test.MoreAsserts; import android.util.SparseArray; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -36,13 +36,13 @@ public class CachedContentIndexTest extends InstrumentationTestCase { @Override public void setUp() throws Exception { - cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); + cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); } @Override protected void tearDown() throws Exception { - TestUtil.recursiveDelete(cacheDir); + Util.recursiveDelete(cacheDir); } public void testAddGetRemove() throws Exception { diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java index 799027f4b5..f2e199578c 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream.cache; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; import org.mockito.Mock; @@ -49,13 +50,13 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX); - cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); + cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); } @Override protected void tearDown() throws Exception { - TestUtil.recursiveDelete(cacheDir); + Util.recursiveDelete(cacheDir); } public void testGetRegion_noSpansInCache() { diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java index 0b40cd7735..8c684b1cb3 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer2.upstream.cache; import android.test.InstrumentationTestCase; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -48,13 +48,13 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); + cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); } @Override protected void tearDown() throws Exception { - TestUtil.recursiveDelete(cacheDir); + Util.recursiveDelete(cacheDir); } public void testCacheFile() throws Exception { diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index 93d7a123bc..1a6beeb6ba 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.upstream.cache; import android.test.InstrumentationTestCase; import android.test.MoreAsserts; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.FileInputStream; @@ -39,12 +38,12 @@ public class SimpleCacheTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - this.cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); + cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); } @Override protected void tearDown() throws Exception { - TestUtil.recursiveDelete(cacheDir); + Util.recursiveDelete(cacheDir); } public void testCommittingOneFile() throws Exception { diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java index 7cdbb9a5b1..6c5d7c76f7 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.util; import android.test.InstrumentationTestCase; -import com.google.android.exoplayer2.testutil.TestUtil; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -34,14 +33,14 @@ public class AtomicFileTest extends InstrumentationTestCase { @Override public void setUp() throws Exception { - tempFolder = TestUtil.createTempFolder(getInstrumentation().getContext()); + tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); file = new File(tempFolder, "atomicFile"); atomicFile = new AtomicFile(file); } @Override protected void tearDown() throws Exception { - TestUtil.recursiveDelete(tempFolder); + Util.recursiveDelete(tempFolder); } public void testDelete() throws Exception { diff --git a/library/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/src/main/java/com/google/android/exoplayer2/util/Util.java index 8a32c54356..60846170b9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -36,6 +36,7 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import java.io.ByteArrayOutputStream; import java.io.Closeable; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; @@ -946,6 +947,24 @@ public final class Util { throw (T) t; } + /** Recursively deletes a directory and its content. */ + public static void recursiveDelete(File fileOrDirectory) { + if (fileOrDirectory.isDirectory()) { + for (File child : fileOrDirectory.listFiles()) { + recursiveDelete(child); + } + } + fileOrDirectory.delete(); + } + + /** Creates an empty directory in the directory returned by {@link Context#getCacheDir()}. */ + public static File createTempDirectory(Context context, String prefix) throws IOException { + File tempFile = File.createTempFile(prefix, null, context.getCacheDir()); + tempFile.delete(); // Delete the temp file. + tempFile.mkdir(); // Create a directory with the same name. + return tempFile; + } + /** * Returns the result of updating a CRC with the specified bytes in a "most significant bit first" * order. diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index fd971892b4..75a4a01923 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.testutil; import android.app.Instrumentation; -import android.content.Context; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.Extractor; @@ -25,7 +24,6 @@ import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; @@ -375,21 +373,4 @@ public class TestUtil { } } - public static void recursiveDelete(File fileOrDirectory) { - if (fileOrDirectory.isDirectory()) { - for (File child : fileOrDirectory.listFiles()) { - recursiveDelete(child); - } - } - fileOrDirectory.delete(); - } - - /** Creates an empty folder in the application specific cache directory. */ - public static File createTempFolder(Context context) throws IOException { - File tempFolder = File.createTempFile("ExoPlayerTest", null, context.getCacheDir()); - Assert.assertTrue(tempFolder.delete()); - Assert.assertTrue(tempFolder.mkdir()); - return tempFolder; - } - } From 76c9968211c704fa7b8811bbc1a4f8362f6cf32b Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 15 Mar 2017 08:47:48 -0700 Subject: [PATCH 130/140] Add RepresentationKey and DashManifest copy method RepresentationKey defines a representation location in a DashManifest. DashManifest copy method creates a copy of the manifest which includes only the representations pointed by the given RepresentationKeys. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150195990 --- .../dash/manifest/DashManifestTest.java | 197 ++++++++++++++++++ .../source/dash/manifest/DashManifest.java | 62 ++++++ .../dash/manifest/RepresentationKey.java | 82 ++++++++ 3 files changed, 341 insertions(+) create mode 100644 library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationKey.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java new file mode 100644 index 0000000000..c796025b08 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2017 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.source.dash.manifest; + +import android.net.Uri; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import junit.framework.TestCase; + +/** + * Unit tests for {@link DashManifest}. + */ +public class DashManifestTest extends TestCase { + + private static final UtcTimingElement DUMMY_UTC_TIMING = new UtcTimingElement("", ""); + private static final List DUMMY_ACCESSIBILITY_DESCRIPTORS = + Collections.emptyList(); + private static final SingleSegmentBase DUMMY_SEGMENT_BASE = new SingleSegmentBase(); + private static final Format DUMMY_FORMAT = Format.createSampleFormat("", "", 0); + + public void testCopy() throws Exception { + Representation[][][] representations = newRepresentations(3, 2, 3); + DashManifest sourceManifest = newDashManifest(10, + newPeriod("1", 1, + newAdaptationSet(2, representations[0][0]), + newAdaptationSet(3, representations[0][1])), + newPeriod("4", 4, + newAdaptationSet(5, representations[1][0]), + newAdaptationSet(6, representations[1][1])), + newPeriod("7", 7, + newAdaptationSet(8, representations[2][0]), + newAdaptationSet(9, representations[2][1]))); + + List keys = Arrays.asList( + new RepresentationKey(0, 0, 0), + new RepresentationKey(0, 0, 1), + new RepresentationKey(0, 1, 2), + + new RepresentationKey(1, 0, 1), + new RepresentationKey(1, 1, 0), + new RepresentationKey(1, 1, 2), + + new RepresentationKey(2, 0, 1), + new RepresentationKey(2, 0, 2), + new RepresentationKey(2, 1, 0)); + // Keys don't need to be in any particular order + Collections.shuffle(keys, new Random(0)); + + DashManifest copyManifest = sourceManifest.copy(keys); + + DashManifest expectedManifest = newDashManifest(10, + newPeriod("1", 1, + newAdaptationSet(2, representations[0][0][0], representations[0][0][1]), + newAdaptationSet(3, representations[0][1][2])), + newPeriod("4", 4, + newAdaptationSet(5, representations[1][0][1]), + newAdaptationSet(6, representations[1][1][0], representations[1][1][2])), + newPeriod("7", 7, + newAdaptationSet(8, representations[2][0][1], representations[2][0][2]), + newAdaptationSet(9, representations[2][1][0]))); + assertManifestEquals(expectedManifest, copyManifest); + } + + public void testCopySameAdaptationIndexButDifferentPeriod() throws Exception { + Representation[][][] representations = newRepresentations(2, 1, 1); + DashManifest sourceManifest = newDashManifest(10, + newPeriod("1", 1, + newAdaptationSet(2, representations[0][0])), + newPeriod("4", 4, + newAdaptationSet(5, representations[1][0]))); + + DashManifest copyManifest = sourceManifest.copy(Arrays.asList( + new RepresentationKey(0, 0, 0), + new RepresentationKey(1, 0, 0))); + + DashManifest expectedManifest = newDashManifest(10, + newPeriod("1", 1, + newAdaptationSet(2, representations[0][0])), + newPeriod("4", 4, + newAdaptationSet(5, representations[1][0]))); + assertManifestEquals(expectedManifest, copyManifest); + } + + public void testCopySkipPeriod() throws Exception { + Representation[][][] representations = newRepresentations(3, 2, 3); + DashManifest sourceManifest = newDashManifest(10, + newPeriod("1", 1, + newAdaptationSet(2, representations[0][0]), + newAdaptationSet(3, representations[0][1])), + newPeriod("4", 4, + newAdaptationSet(5, representations[1][0]), + newAdaptationSet(6, representations[1][1])), + newPeriod("7", 7, + newAdaptationSet(8, representations[2][0]), + newAdaptationSet(9, representations[2][1]))); + + DashManifest copyManifest = sourceManifest.copy(Arrays.asList( + new RepresentationKey(0, 0, 0), + new RepresentationKey(0, 0, 1), + new RepresentationKey(0, 1, 2), + + new RepresentationKey(2, 0, 1), + new RepresentationKey(2, 0, 2), + new RepresentationKey(2, 1, 0))); + + DashManifest expectedManifest = newDashManifest(7, + newPeriod("1", 1, + newAdaptationSet(2, representations[0][0][0], representations[0][0][1]), + newAdaptationSet(3, representations[0][1][2])), + newPeriod("7", 4, + newAdaptationSet(8, representations[2][0][1], representations[2][0][2]), + newAdaptationSet(9, representations[2][1][0]))); + assertManifestEquals(expectedManifest, copyManifest); + } + + private static void assertManifestEquals(DashManifest expected, DashManifest actual) { + assertEquals(expected.availabilityStartTime, actual.availabilityStartTime); + assertEquals(expected.duration, actual.duration); + assertEquals(expected.minBufferTime, actual.minBufferTime); + assertEquals(expected.dynamic, actual.dynamic); + assertEquals(expected.minUpdatePeriod, actual.minUpdatePeriod); + assertEquals(expected.timeShiftBufferDepth, actual.timeShiftBufferDepth); + assertEquals(expected.suggestedPresentationDelay, actual.suggestedPresentationDelay); + assertEquals(expected.utcTiming, actual.utcTiming); + assertEquals(expected.location, actual.location); + assertEquals(expected.getPeriodCount(), actual.getPeriodCount()); + for (int i = 0; i < expected.getPeriodCount(); i++) { + Period expectedPeriod = expected.getPeriod(i); + Period actualPeriod = actual.getPeriod(i); + assertEquals(expectedPeriod.id, actualPeriod.id); + assertEquals(expectedPeriod.startMs, actualPeriod.startMs); + List expectedAdaptationSets = expectedPeriod.adaptationSets; + List actualAdaptationSets = actualPeriod.adaptationSets; + assertEquals(expectedAdaptationSets.size(), actualAdaptationSets.size()); + for (int j = 0; j < expectedAdaptationSets.size(); j++) { + AdaptationSet expectedAdaptationSet = expectedAdaptationSets.get(j); + AdaptationSet actualAdaptationSet = actualAdaptationSets.get(j); + assertEquals(expectedAdaptationSet.id, actualAdaptationSet.id); + assertEquals(expectedAdaptationSet.type, actualAdaptationSet.type); + assertEquals(expectedAdaptationSet.accessibilityDescriptors, + actualAdaptationSet.accessibilityDescriptors); + assertEquals(expectedAdaptationSet.representations, actualAdaptationSet.representations); + } + } + } + + private static Representation[][][] newRepresentations(int periodCount, int adaptationSetCounts, + int representationCounts) { + Representation[][][] representations = new Representation[periodCount][][]; + for (int i = 0; i < periodCount; i++) { + representations[i] = new Representation[adaptationSetCounts][]; + for (int j = 0; j < adaptationSetCounts; j++) { + representations[i][j] = new Representation[representationCounts]; + for (int k = 0; k < representationCounts; k++) { + representations[i][j][k] = newRepresentation(); + } + } + } + return representations; + } + + private static Representation newRepresentation() { + return Representation.newInstance("", 0, DUMMY_FORMAT, "", DUMMY_SEGMENT_BASE); + } + + private static DashManifest newDashManifest(int duration, Period... periods) { + return new DashManifest(0, duration, 1, false, 2, 3, 4, DUMMY_UTC_TIMING, Uri.EMPTY, + Arrays.asList(periods)); + } + + private static Period newPeriod(String id, int startMs, AdaptationSet... adaptationSets) { + return new Period(id, startMs, Arrays.asList(adaptationSets)); + } + + private static AdaptationSet newAdaptationSet(int seed, Representation... representations) { + return new AdaptationSet(++seed, ++seed, Arrays.asList(representations), + DUMMY_ACCESSIBILITY_DESCRIPTORS); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java index 0c713b949a..eb51c8312d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java @@ -17,7 +17,9 @@ package com.google.android.exoplayer2.source.dash.manifest; import android.net.Uri; import com.google.android.exoplayer2.C; +import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedList; import java.util.List; /** @@ -79,4 +81,64 @@ public class DashManifest { return C.msToUs(getPeriodDurationMs(index)); } + /** + * Creates a copy of this manifest which includes only the representations identified by the given + * keys. + * + * @param representationKeys List of keys for the representations to be included in the copy. + * @return A copy of this manifest with the selected representations. + * @throws IndexOutOfBoundsException If a key has an invalid index. + */ + public final DashManifest copy(List representationKeys) { + LinkedList keys = new LinkedList<>(representationKeys); + Collections.sort(keys); + keys.add(new RepresentationKey(-1, -1, -1)); // Add a stopper key to the end + + ArrayList copyPeriods = new ArrayList<>(); + long shiftMs = 0; + for (int periodIndex = 0; periodIndex < getPeriodCount(); periodIndex++) { + if (keys.peek().periodIndex != periodIndex) { + // No representations selected in this period. + long periodDurationMs = getPeriodDurationMs(periodIndex); + if (periodDurationMs != C.TIME_UNSET) { + shiftMs += periodDurationMs; + } + } else { + Period period = getPeriod(periodIndex); + ArrayList copyAdaptationSets = + copyAdaptationSets(period.adaptationSets, keys); + copyPeriods.add(new Period(period.id, period.startMs - shiftMs, copyAdaptationSets)); + } + } + long newDuration = duration != C.TIME_UNSET ? duration - shiftMs : C.TIME_UNSET; + return new DashManifest(availabilityStartTime, newDuration, minBufferTime, dynamic, + minUpdatePeriod, timeShiftBufferDepth, suggestedPresentationDelay, utcTiming, location, + copyPeriods); + } + + private static ArrayList copyAdaptationSets( + List adaptationSets, LinkedList keys) { + RepresentationKey key = keys.poll(); + int periodIndex = key.periodIndex; + ArrayList copyAdaptationSets = new ArrayList<>(); + do { + int adaptationSetIndex = key.adaptationSetIndex; + AdaptationSet adaptationSet = adaptationSets.get(adaptationSetIndex); + + List representations = adaptationSet.representations; + ArrayList copyRepresentations = new ArrayList<>(); + do { + Representation representation = representations.get(key.representationIndex); + copyRepresentations.add(representation); + key = keys.poll(); + } while(key.periodIndex == periodIndex && key.adaptationSetIndex == adaptationSetIndex); + + copyAdaptationSets.add(new AdaptationSet(adaptationSet.id, adaptationSet.type, + copyRepresentations, adaptationSet.accessibilityDescriptors)); + } while(key.periodIndex == periodIndex); + // Add back the last key which doesn't belong to the period being processed + keys.addFirst(key); + return copyAdaptationSets; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationKey.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationKey.java new file mode 100644 index 0000000000..51451a83c2 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationKey.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2017 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.source.dash.manifest; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Uniquely identifies a {@link Representation} in a {@link DashManifest}. + */ +public final class RepresentationKey implements Parcelable, Comparable { + + public final int periodIndex; + public final int adaptationSetIndex; + public final int representationIndex; + + public RepresentationKey(int periodIndex, int adaptationSetIndex, int representationIndex) { + this.periodIndex = periodIndex; + this.adaptationSetIndex = adaptationSetIndex; + this.representationIndex = representationIndex; + } + + @Override + public String toString() { + return periodIndex + "." + adaptationSetIndex + "." + representationIndex; + } + + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(periodIndex); + dest.writeInt(adaptationSetIndex); + dest.writeInt(representationIndex); + } + + public static final Creator CREATOR = + new Creator() { + @Override + public RepresentationKey createFromParcel(Parcel in) { + return new RepresentationKey(in.readInt(), in.readInt(), in.readInt()); + } + + @Override + public RepresentationKey[] newArray(int size) { + return new RepresentationKey[size]; + } + }; + + // Comparable implementation. + + @Override + public int compareTo(RepresentationKey o) { + int result = periodIndex - o.periodIndex; + if (result == 0) { + result = adaptationSetIndex - o.adaptationSetIndex; + if (result == 0) { + result = representationIndex - o.representationIndex; + } + } + return result; + } + +} From ccc5e472b0a4c0e07cbf5760b017b9dd720e5d80 Mon Sep 17 00:00:00 2001 From: vigneshv Date: Wed, 15 Mar 2017 10:28:49 -0700 Subject: [PATCH 131/140] extensions/vp9: Fix open source build scripts Fixes github issue #2339 [https://github.com/google/ExoPlayer/issues/2339] Fixes github issue #2551 [https://github.com/google/ExoPlayer/issues/2551] * Update the instructions to check out specific versions of libvpx and libyuv that are known to work with our build scripts. * Forcing a particular version of libyuv because recent versions of libyuv are dependent on libjpeg (which isn't needed for the purpose of this extension). * Going forward, let's keep generate_libvpx_android_configs.sh in sync with whatever version is specifed in the instructions in README.md (as of now it is v1.6.1). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150208517 --- extensions/vp9/README.md | 16 +++++++++++++++- .../main/jni/generate_libvpx_android_configs.sh | 12 ++++++++---- extensions/vp9/src/main/jni/libvpx.mk | 10 ++++++++-- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 97c6b46280..90ded8fdc0 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -40,6 +40,18 @@ git clone https://chromium.googlesource.com/webm/libvpx libvpx && \ git clone https://chromium.googlesource.com/libyuv/libyuv libyuv ``` +* Checkout the appropriate branches of libvpx and libyuv (the scripts and + makefiles bundled in this repo are known to work only at these versions of the + libraries - we will update this periodically as newer versions of + libvpx/libyuv are released): + +``` +cd "${VP9_EXT_PATH}/jni/libvpx" && \ +git checkout tags/v1.6.1 -b v1.6.1 && \ +cd "${VP9_EXT_PATH}/jni/libyuv" && \ +git checkout e2611a73 +``` + * Run a script that generates necessary configuration files for libvpx: ``` @@ -79,5 +91,7 @@ dependencies { `generate_libvpx_android_configs.sh` * Clean and re-build the project. * If you want to use your own version of libvpx or libyuv, place it in - `${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. + `${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But + please note that `generate_libvpx_android_configs.sh` and the makefiles need + to be modified to work with arbitrary versions of libvpx and libyuv. diff --git a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh index f0fb2761db..566396e0bf 100755 --- a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh +++ b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh @@ -40,7 +40,7 @@ config[0]+=" --enable-neon-asm" arch[1]="armeabi" config[1]="--target=armv7-android-gcc --sdk-path=$ndk --disable-neon" -config[1]+=" --disable-neon-asm --disable-media" +config[1]+=" --disable-neon-asm" arch[2]="mips" config[2]="--force-target=mips32-android-gcc --sdk-path=$ndk" @@ -78,12 +78,12 @@ convert_asm() { for i in $(seq 0 ${limit}); do while read file; do case "${file}" in - *.asm.s) + *.asm.[sS]) # Some files may already have been processed (there are duplicated # .asm.s files for vp8 in the armeabi/armeabi-v7a configurations). file="libvpx/${file}" if [[ ! -e "${file}" ]]; then - asm_file="${file%.s}" + asm_file="${file%.[sS]}" cat "${asm_file}" | libvpx/build/make/ads2gas.pl > "${file}" remove_trailing_whitespace "${file}" rm "${asm_file}" @@ -105,7 +105,11 @@ for i in $(seq 0 ${limit}); do echo "configure ${config[${i}]} ${common_params}" ../../libvpx/configure ${config[${i}]} ${common_params} rm -f libvpx_srcs.txt - make libvpx_srcs.txt + for f in ${allowed_files}; do + # the build system supports multiple different configurations. avoid + # failing out when, for example, vp8_rtcd.h is not part of a configuration + make "${f}" || true + done # remove files that aren't needed rm -rf !(${allowed_files// /|}) diff --git a/extensions/vp9/src/main/jni/libvpx.mk b/extensions/vp9/src/main/jni/libvpx.mk index 6cc706ffa8..887de56218 100644 --- a/extensions/vp9/src/main/jni/libvpx.mk +++ b/extensions/vp9/src/main/jni/libvpx.mk @@ -35,16 +35,22 @@ LOCAL_SRC_FILES += $(addprefix libvpx/, $(filter-out vpx_config.c, \ $(filter %.c, $(libvpx_codec_srcs)))) # include assembly files if they exist -# "%.asm.s" covers neon assembly and "%.asm" covers x86 assembly +# "%.asm.[sS]" covers neon assembly and "%.asm" covers x86 assembly LOCAL_SRC_FILES += $(addprefix libvpx/, \ $(filter %.asm.s %.asm, $(libvpx_codec_srcs))) +LOCAL_SRC_FILES += $(addprefix libvpx/, \ + $(filter %.asm.S %.asm, $(libvpx_codec_srcs))) ifneq ($(findstring armeabi-v7a, $(TARGET_ARCH_ABI)),) -# append .neon to *_neon.c and *.s +# append .neon to *_neon.c and *.[sS] LOCAL_SRC_FILES := $(subst _neon.c,_neon.c.neon,$(LOCAL_SRC_FILES)) LOCAL_SRC_FILES := $(subst .s,.s.neon,$(LOCAL_SRC_FILES)) +LOCAL_SRC_FILES := $(subst .S,.S.neon,$(LOCAL_SRC_FILES)) endif +# remove duplicates +LOCAL_SRC_FILES := $(sort $(LOCAL_SRC_FILES)) + LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/libvpx \ $(LOCAL_PATH)/libvpx/vpx From a6cea62c1b78d0fef48cdb73b8f41714cf43cd6e Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 Mar 2017 10:28:50 -0700 Subject: [PATCH 132/140] Add gradle instructions to GVR readme + clean up FFMPEG readme Note: Depending on the GVR extension via gradle wont work until we actually push a release ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150208524 --- extensions/ffmpeg/README.md | 9 +++------ extensions/gvr/README.md | 30 +++++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index 0d669f826d..beafcb6a96 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -31,9 +31,7 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main" NDK_PATH="" ``` -* Fetch and build FFmpeg. - -For example, to fetch and build for armv7a: +* Fetch and build FFmpeg. For example, to fetch and build for armv7a: ``` cd "${FFMPEG_EXT_PATH}/jni" && \ @@ -69,15 +67,14 @@ make -j4 && \ make install-libs ``` -* Build the JNI native libraries. +* Build the JNI native libraries. Repeat this step for any other architectures + you need to support. ``` cd "${FFMPEG_EXT_PATH}"/jni && \ ${NDK_PATH}/ndk-build APP_ABI=armeabi-v7a -j4 ``` -Repeat these steps for any other architectures you need to support. - * In your project, you can add a dependency on the extension by using a rule like this: diff --git a/extensions/gvr/README.md b/extensions/gvr/README.md index 0fe33a6755..bae5de4812 100644 --- a/extensions/gvr/README.md +++ b/extensions/gvr/README.md @@ -6,13 +6,33 @@ The GVR extension wraps the [Google VR SDK for Android][]. It provides a GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering of surround sound and ambisonic soundfields. -## Instructions ## +## Using the extension ## -If using SimpleExoPlayer, override SimpleExoPlayer.buildAudioProcessors to -return a GvrAudioProcessor. +The easiest way to use the extension is to add it as a gradle dependency. You +need to make sure you have the jcenter repository included in the `build.gradle` +file in the root of your project: -If constructing renderers directly, pass a GvrAudioProcessor to -MediaCodecAudioRenderer's constructor. +```gradle +repositories { + jcenter() +} +``` + +Next, include the following in your module's `build.gradle` file: + +```gradle +compile 'com.google.android.exoplayer:extension-gvr:rX.X.X' +``` + +where `rX.X.X` is the version, which must match the version of the ExoPlayer +library being used. + +## Using GvrAudioProcessor ## + +* If using SimpleExoPlayer, override SimpleExoPlayer.buildAudioProcessors to + return a GvrAudioProcessor. +* If constructing renderers directly, pass a GvrAudioProcessor to + MediaCodecAudioRenderer's constructor. [Google VR SDK for Android]: https://developers.google.com/vr/android/ [GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround From 7c9b771ec890bfdfd76859e53a83311d2edc564e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 15 Mar 2017 13:17:07 -0700 Subject: [PATCH 133/140] Create HlsMediaSource.Manifest to hold playlist information This allows the user to get the HlsMasterPlaylist through Exoplayer.getCurrentManifest(). Issue:#2537 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150232455 --- .../exoplayer2/source/hls/HlsManifest.java | 44 +++++++++++++++++++ .../exoplayer2/source/hls/HlsMediaSource.java | 3 +- 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/source/hls/HlsManifest.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsManifest.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsManifest.java new file mode 100644 index 0000000000..81d63fd4ad --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsManifest.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2017 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.source.hls; + +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; + +/** + * Holds a master playlist along with a snapshot of one of its media playlists. + */ +public final class HlsManifest { + + /** + * The master playlist of an HLS stream. + */ + public final HlsMasterPlaylist masterPlaylist; + /** + * A snapshot of a media playlist referred to by {@link #masterPlaylist}. + */ + public final HlsMediaPlaylist mediaPlaylist; + + /** + * @param masterPlaylist The master playlist. + * @param mediaPlaylist The media playlist. + */ + HlsManifest(HlsMasterPlaylist masterPlaylist, HlsMediaPlaylist mediaPlaylist) { + this.masterPlaylist = masterPlaylist; + this.mediaPlaylist = mediaPlaylist; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 11de1adfb1..3cd9f19522 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -129,7 +129,8 @@ public final class HlsMediaSource implements MediaSource, timeline = new SinglePeriodTimeline(playlist.startTimeUs + playlist.durationUs, playlist.durationUs, playlist.startTimeUs, windowDefaultStartPositionUs, true, false); } - sourceListener.onSourceInfoRefreshed(timeline, playlist); + sourceListener.onSourceInfoRefreshed(timeline, + new HlsManifest(playlistTracker.getMasterPlaylist(), playlist)); } } From 8cad99ba98304eaaf689dabf2a9eac66300fa320 Mon Sep 17 00:00:00 2001 From: anjalibh Date: Wed, 15 Mar 2017 15:59:46 -0700 Subject: [PATCH 134/140] Add lightweight dithering to 10->8 bit conversion. Also optimize / 4 and % 4. I assumed the compiler would do this automatically but the performance bump implies it's not doing that. Before | Optimized No dither: 4.8 ms | 3.5 ms Dither : 9.6 ms | 4.2 ms Before: https://drive.google.com[]file/d/0B07DogHRdEHcaXVobi1wZ2wxeUE/view?usp=sharing After: https://drive.google.com[]file/d/0B07DogHRdEHcVS1PN05kaU1odm8/view?usp=sharing Known issue: The remainder from the last Y pixel will leak into the first U pixel. Also U and V remainders leak into each other but I don't think it causes any perceptual difference. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150255151 --- extensions/vp9/src/main/jni/vpx_jni.cc | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index 67fd250fdc..c091091389 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -196,6 +196,7 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { const int32_t uvHeight = (img->d_h + 1) / 2; const uint64_t yLength = img->stride[VPX_PLANE_Y] * img->d_h; const uint64_t uvLength = img->stride[VPX_PLANE_U] * uvHeight; + int sample = 0; if (img->fmt == VPX_IMG_FMT_I42016) { // HBD planar 420. // Note: The stride for BT2020 is twice of what we use so this is wasting // memory. The long term goal however is to upload half-float/short so @@ -206,7 +207,11 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { img->planes[VPX_PLANE_Y] + img->stride[VPX_PLANE_Y] * y); int8_t* destBase = data + img->stride[VPX_PLANE_Y] * y; for (int x = 0; x < img->d_w; x++) { - *destBase++ = *srcBase++ / 4; + // Lightweight dither. Carryover the remainder of each 10->8 bit + // conversion to the next pixel. + sample += *srcBase++; + *destBase++ = sample >> 2; + sample = sample & 3; // Remainder. } } // UV @@ -220,8 +225,13 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { int8_t* destVBase = data + yLength + uvLength + img->stride[VPX_PLANE_V] * y; for (int x = 0; x < uvWidth; x++) { - *destUBase++ = *srcUBase++ / 4; - *destVBase++ = *srcVBase++ / 4; + // Lightweight dither. Carryover the remainder of each 10->8 bit + // conversion to the next pixel. + sample += *srcUBase++; + *destUBase++ = sample >> 2; + sample = (*srcVBase++) + (sample & 3); // srcV + previousRemainder. + *destVBase++ = sample >> 2; + sample = sample & 3; // Remainder. } } } else { From cadce0ef3f539a36dd8257189301538c050c5964 Mon Sep 17 00:00:00 2001 From: adrianv Date: Wed, 15 Mar 2017 20:23:51 -0700 Subject: [PATCH 135/140] Add @Nullable annotation for an optional field in CacheDataSource's constructor. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150278031 --- .../android/exoplayer2/upstream/cache/CacheDataSource.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index dc8797362f..a2e4382e0c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.upstream.cache; import android.net.Uri; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSource; @@ -89,7 +90,7 @@ public final class CacheDataSource implements DataSource { private final DataSource cacheReadDataSource; private final DataSource cacheWriteDataSource; private final DataSource upstreamDataSource; - private final EventListener eventListener; + @Nullable private final EventListener eventListener; private final boolean blockOnCache; private final boolean ignoreCacheOnError; @@ -149,7 +150,7 @@ public final class CacheDataSource implements DataSource { * @param eventListener An optional {@link EventListener} to receive events. */ public CacheDataSource(Cache cache, DataSource upstream, DataSource cacheReadDataSource, - DataSink cacheWriteDataSink, @Flags int flags, EventListener eventListener) { + DataSink cacheWriteDataSink, @Flags int flags, @Nullable EventListener eventListener) { this.cache = cache; this.cacheReadDataSource = cacheReadDataSource; this.blockOnCache = (flags & FLAG_BLOCK_ON_CACHE) != 0; From ce5c0c18f952caa19b863345252fd267804d7e83 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 16 Mar 2017 03:36:02 -0700 Subject: [PATCH 136/140] Rename AdaptiveVideoTrackSelection to AdaptiveTrackSelection This will allow us to use the same class for Audio adaptation. Issue:#1975 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150302561 --- .../exoplayer2/demo/PlayerActivity.java | 4 +- .../exoplayer2/demo/TrackSelectionHelper.java | 14 +++---- ...ction.java => AdaptiveTrackSelection.java} | 37 ++++++++++--------- .../trackselection/DefaultTrackSelector.java | 3 +- .../playbacktests/util/ExoHostedTest.java | 4 +- 5 files changed, 32 insertions(+), 30 deletions(-) rename library/src/main/java/com/google/android/exoplayer2/trackselection/{AdaptiveVideoTrackSelection.java => AdaptiveTrackSelection.java} (88%) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index ba9c9352d8..ffe6bb1fee 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -55,7 +55,7 @@ import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.trackselection.AdaptiveVideoTrackSelection; +import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -256,7 +256,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay : SimpleExoPlayer.EXTENSION_RENDERER_MODE_ON) : SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF; TrackSelection.Factory videoTrackSelectionFactory = - new AdaptiveVideoTrackSelection.Factory(BANDWIDTH_METER); + new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); trackSelectionHelper = new TrackSelectionHelper(trackSelector, videoTrackSelectionFactory); player = ExoPlayerFactory.newSimpleInstance(this, trackSelector, new DefaultLoadControl(), diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java b/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java index 338544b1ed..576eb23c9d 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java @@ -51,7 +51,7 @@ import java.util.Locale; private static final TrackSelection.Factory RANDOM_FACTORY = new RandomTrackSelection.Factory(); private final MappingTrackSelector selector; - private final TrackSelection.Factory adaptiveVideoTrackSelectionFactory; + private final TrackSelection.Factory adaptiveTrackSelectionFactory; private MappedTrackInfo trackInfo; private int rendererIndex; @@ -67,13 +67,13 @@ import java.util.Locale; /** * @param selector The track selector. - * @param adaptiveVideoTrackSelectionFactory A factory for adaptive video {@link TrackSelection}s, - * or null if the selection helper should not support adaptive video. + * @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s, or null + * if the selection helper should not support adaptive tracks. */ public TrackSelectionHelper(MappingTrackSelector selector, - TrackSelection.Factory adaptiveVideoTrackSelectionFactory) { + TrackSelection.Factory adaptiveTrackSelectionFactory) { this.selector = selector; - this.adaptiveVideoTrackSelectionFactory = adaptiveVideoTrackSelectionFactory; + this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory; } /** @@ -92,7 +92,7 @@ import java.util.Locale; trackGroups = trackInfo.getTrackGroups(rendererIndex); trackGroupsAdaptive = new boolean[trackGroups.length]; for (int i = 0; i < trackGroups.length; i++) { - trackGroupsAdaptive[i] = adaptiveVideoTrackSelectionFactory != null + trackGroupsAdaptive[i] = adaptiveTrackSelectionFactory != null && trackInfo.getAdaptiveSupport(rendererIndex, i, false) != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED && trackGroups.get(i).length > 1; @@ -271,7 +271,7 @@ import java.util.Locale; private void setOverride(int group, int[] tracks, boolean enableRandomAdaptation) { TrackSelection.Factory factory = tracks.length == 1 ? FIXED_FACTORY - : (enableRandomAdaptation ? RANDOM_FACTORY : adaptiveVideoTrackSelectionFactory); + : (enableRandomAdaptation ? RANDOM_FACTORY : adaptiveTrackSelectionFactory); override = new SelectionOverride(factory, group, tracks); } diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveVideoTrackSelection.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java similarity index 88% rename from library/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveVideoTrackSelection.java rename to library/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index 868303cc5b..dc78e28e56 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveVideoTrackSelection.java +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -24,13 +24,13 @@ import com.google.android.exoplayer2.upstream.BandwidthMeter; import java.util.List; /** - * A bandwidth based adaptive {@link TrackSelection} for video, whose selected track is updated to - * be the one of highest quality given the current network conditions and the state of the buffer. + * A bandwidth based adaptive {@link TrackSelection}, whose selected track is updated to be the one + * of highest quality given the current network conditions and the state of the buffer. */ -public class AdaptiveVideoTrackSelection extends BaseTrackSelection { +public class AdaptiveTrackSelection extends BaseTrackSelection { /** - * Factory for {@link AdaptiveVideoTrackSelection} instances. + * Factory for {@link AdaptiveTrackSelection} instances. */ public static final class Factory implements TrackSelection.Factory { @@ -79,8 +79,8 @@ public class AdaptiveVideoTrackSelection extends BaseTrackSelection { } @Override - public AdaptiveVideoTrackSelection createTrackSelection(TrackGroup group, int... tracks) { - return new AdaptiveVideoTrackSelection(group, tracks, bandwidthMeter, maxInitialBitrate, + public AdaptiveTrackSelection createTrackSelection(TrackGroup group, int... tracks) { + return new AdaptiveTrackSelection(group, tracks, bandwidthMeter, maxInitialBitrate, minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs, minDurationToRetainAfterDiscardMs, bandwidthFraction); } @@ -104,12 +104,12 @@ public class AdaptiveVideoTrackSelection extends BaseTrackSelection { private int reason; /** - * @param group The {@link TrackGroup}. Must not be null. + * @param group The {@link TrackGroup}. * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be - * null or empty. May be in any order. + * empty. May be in any order. * @param bandwidthMeter Provides an estimate of the currently available bandwidth. */ - public AdaptiveVideoTrackSelection(TrackGroup group, int[] tracks, + public AdaptiveTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter) { this (group, tracks, bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, @@ -118,9 +118,9 @@ public class AdaptiveVideoTrackSelection extends BaseTrackSelection { } /** - * @param group The {@link TrackGroup}. Must not be null. + * @param group The {@link TrackGroup}. * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be - * null or empty. May be in any order. + * empty. May be in any order. * @param bandwidthMeter Provides an estimate of the currently available bandwidth. * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed when a * bandwidth estimate is unavailable. @@ -136,7 +136,7 @@ public class AdaptiveVideoTrackSelection extends BaseTrackSelection { * consider available for use. Setting to a value less than 1 is recommended to account * for inaccuracies in the bandwidth estimator. */ - public AdaptiveVideoTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter, + public AdaptiveTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter, int maxInitialBitrate, long minDurationForQualityIncreaseMs, long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs, float bandwidthFraction) { @@ -208,15 +208,18 @@ public class AdaptiveVideoTrackSelection extends BaseTrackSelection { } int idealSelectedIndex = determineIdealSelectedIndex(SystemClock.elapsedRealtime()); Format idealFormat = getFormat(idealSelectedIndex); - // Discard from the first SD chunk beyond minDurationToRetainAfterDiscardUs whose resolution and - // bitrate are both lower than the ideal track. + // If the chunks contain video, discard from the first SD chunk beyond + // minDurationToRetainAfterDiscardUs whose resolution and bitrate are both lower than the ideal + // track. for (int i = 0; i < queueSize; i++) { MediaChunk chunk = queue.get(i); + Format format = chunk.trackFormat; long durationBeforeThisChunkUs = chunk.startTimeUs - playbackPositionUs; if (durationBeforeThisChunkUs >= minDurationToRetainAfterDiscardUs - && chunk.trackFormat.bitrate < idealFormat.bitrate - && chunk.trackFormat.height < idealFormat.height - && chunk.trackFormat.height < 720 && chunk.trackFormat.width < 1280) { + && format.bitrate < idealFormat.bitrate + && format.height != Format.NO_VALUE && format.height < 720 + && format.width != Format.NO_VALUE && format.width < 1280 + && format.height < idealFormat.height) { return i; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 46e2346628..1fa372ca0a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -432,8 +432,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { params.maxVideoHeight, params.maxVideoBitrate, params.allowNonSeamlessAdaptiveness, params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight, params.orientationMayChange, adaptiveVideoTrackSelectionFactory, - params.exceedVideoConstraintsIfNecessary, - params.exceedRendererCapabilitiesIfNecessary); + params.exceedVideoConstraintsIfNecessary, params.exceedRendererCapabilitiesIfNecessary); } } diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java index 74262f4422..87c55e9248 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java +++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java @@ -34,7 +34,7 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.playbacktests.util.HostActivity.HostedTest; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.AdaptiveVideoTrackSelection; +import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -313,7 +313,7 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen @SuppressWarnings("unused") protected MappingTrackSelector buildTrackSelector(HostActivity host, BandwidthMeter bandwidthMeter) { - return new DefaultTrackSelector(new AdaptiveVideoTrackSelection.Factory(bandwidthMeter)); + return new DefaultTrackSelector(new AdaptiveTrackSelection.Factory(bandwidthMeter)); } @SuppressWarnings("unused") From 9b0d24c909bc73f9410fa6f1a50064bf6f350548 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 16 Mar 2017 04:34:22 -0700 Subject: [PATCH 137/140] Fix stuck-buffering state when playing merged media Also added a TODO to track clarifying SequenceableLoader more accurately and auditing existing implementations. Issue: #2396 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150305685 --- .../google/android/exoplayer2/source/ExtractorMediaPeriod.java | 1 + .../com/google/android/exoplayer2/source/SequenceableLoader.java | 1 + 2 files changed, 2 insertions(+) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 31b76a84b3..d843943710 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -354,6 +354,7 @@ import java.io.IOException; sourceListener.onSourceInfoRefreshed( new SinglePeriodTimeline(durationUs, seekMap.isSeekable()), null); } + callback.onContinueLoadingRequested(this); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java b/library/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java index 9aebcece9e..f287153719 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; +// TODO: Clarify the requirements for implementing this interface [Internal ref: b/36250203]. /** * A loader that can proceed in approximate synchronization with other loaders. */ From 2fe478ad6a4034ce661a94b7166d981c6efddf3d Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 16 Mar 2017 05:13:51 -0700 Subject: [PATCH 138/140] Invert DashHostedTest and inner class Builder to make the design more natural Builder class was renamed to DashTestRunner and DashHostedTest moved into it as an inner class. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150307988 --- .../playbacktests/gts/DashHostedTest.java | 449 ----------------- .../playbacktests/gts/DashTest.java | 182 +++---- .../playbacktests/gts/DashTestData.java | 10 + .../playbacktests/gts/DashTestRunner.java | 459 ++++++++++++++++++ .../gts/DashWidevineOfflineTest.java | 23 +- 5 files changed, 579 insertions(+), 544 deletions(-) delete mode 100644 playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java create mode 100644 playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java deleted file mode 100644 index b82a31f6cd..0000000000 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java +++ /dev/null @@ -1,449 +0,0 @@ -/* - * Copyright (C) 2017 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.playbacktests.gts; - -import static com.google.android.exoplayer2.C.WIDEVINE_UUID; - -import android.annotation.TargetApi; -import android.app.Instrumentation; -import android.media.MediaDrm; -import android.media.UnsupportedSchemeException; -import android.net.Uri; -import android.util.Log; -import android.view.Surface; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultLoadControl; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.RendererCapabilities; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.decoder.DecoderCounters; -import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; -import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; -import com.google.android.exoplayer2.drm.MediaDrmCallback; -import com.google.android.exoplayer2.drm.UnsupportedDrmException; -import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; -import com.google.android.exoplayer2.playbacktests.util.ActionSchedule; -import com.google.android.exoplayer2.playbacktests.util.DebugSimpleExoPlayer; -import com.google.android.exoplayer2.playbacktests.util.DecoderCountersUtil; -import com.google.android.exoplayer2.playbacktests.util.ExoHostedTest; -import com.google.android.exoplayer2.playbacktests.util.HostActivity; -import com.google.android.exoplayer2.playbacktests.util.HostActivity.HostedTest; -import com.google.android.exoplayer2.playbacktests.util.MetricsLogger; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; -import com.google.android.exoplayer2.trackselection.FixedTrackSelection; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector; -import com.google.android.exoplayer2.trackselection.RandomTrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.upstream.BandwidthMeter; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.exoplayer2.upstream.TransferListener; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import junit.framework.AssertionFailedError; - -/** - * A {@link HostedTest} for DASH playback tests. - */ -@TargetApi(16) -public final class DashHostedTest extends ExoHostedTest { - - /** {@link DashHostedTest} builder. */ - public static final class Builder { - - private static final long TEST_TIMEOUT_MS = 5 * 60 * 1000; - - private static final String REPORT_NAME = "GtsExoPlayerTestCases"; - private static final String REPORT_OBJECT_NAME = "playbacktest"; - - // Whether adaptive tests should enable video formats beyond those mandated by the Android CDD - // if the device advertises support for them. - private static final boolean ALLOW_ADDITIONAL_VIDEO_FORMATS = Util.SDK_INT >= 24; - - private final String tag; - - private String streamName; - private boolean fullPlaybackNoSeeking; - private String audioFormat; - private boolean canIncludeAdditionalVideoFormats; - private ActionSchedule actionSchedule; - private byte[] offlineLicenseKeySetId; - private String[] videoFormats; - private String manifestUrl; - private boolean useL1Widevine; - private String widevineLicenseUrl; - - public Builder(String tag) { - this.tag = tag; - } - - public Builder setStreamName(String streamName) { - this.streamName = streamName; - return this; - } - - public Builder setFullPlaybackNoSeeking(boolean fullPlaybackNoSeeking) { - this.fullPlaybackNoSeeking = fullPlaybackNoSeeking; - return this; - } - - public Builder setCanIncludeAdditionalVideoFormats( - boolean canIncludeAdditionalVideoFormats) { - this.canIncludeAdditionalVideoFormats = canIncludeAdditionalVideoFormats - && ALLOW_ADDITIONAL_VIDEO_FORMATS; - return this; - } - - public Builder setActionSchedule(ActionSchedule actionSchedule) { - this.actionSchedule = actionSchedule; - return this; - } - - public Builder setOfflineLicenseKeySetId(byte[] offlineLicenseKeySetId) { - this.offlineLicenseKeySetId = offlineLicenseKeySetId; - return this; - } - - public Builder setAudioVideoFormats(String audioFormat, String... videoFormats) { - this.audioFormat = audioFormat; - this.videoFormats = videoFormats; - return this; - } - - public Builder setManifestUrl(String manifestUrl) { - this.manifestUrl = manifestUrl; - return this; - } - - public Builder setWidevineMimeType(String mimeType) { - this.useL1Widevine = isL1WidevineAvailable(mimeType); - this.widevineLicenseUrl = getWidevineLicenseUrl(useL1Widevine); - return this; - } - - private DashHostedTest createDashHostedTest(boolean canIncludeAdditionalVideoFormats, - boolean isCddLimitedRetry, Instrumentation instrumentation) { - MetricsLogger metricsLogger = MetricsLogger.Factory.createDefault(instrumentation, tag, - REPORT_NAME, REPORT_OBJECT_NAME); - return new DashHostedTest(tag, streamName, manifestUrl, metricsLogger, fullPlaybackNoSeeking, - audioFormat, canIncludeAdditionalVideoFormats, isCddLimitedRetry, actionSchedule, - offlineLicenseKeySetId, widevineLicenseUrl, useL1Widevine, videoFormats); - } - - public void runTest(HostActivity activity, Instrumentation instrumentation) { - DashHostedTest test = createDashHostedTest(canIncludeAdditionalVideoFormats, false, - instrumentation); - activity.runTest(test, TEST_TIMEOUT_MS); - // Retry test exactly once if adaptive test fails due to excessive dropped buffers when - // playing non-CDD required formats (b/28220076). - if (test.needsCddLimitedRetry) { - activity.runTest(createDashHostedTest(false, true, instrumentation), TEST_TIMEOUT_MS); - } - } - - } - - private static final String AUDIO_TAG_SUFFIX = ":Audio"; - private static final String VIDEO_TAG_SUFFIX = ":Video"; - static final int VIDEO_RENDERER_INDEX = 0; - static final int AUDIO_RENDERER_INDEX = 1; - - private static final int MIN_LOADABLE_RETRY_COUNT = 10; - private static final int MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES = 10; - private static final float MAX_DROPPED_VIDEO_FRAME_FRACTION = 0.01f; - - private static final String WIDEVINE_LICENSE_URL = - "https://proxy.uat.widevine.com/proxy?provider=widevine_test&video_id="; - private static final String WIDEVINE_SW_CRYPTO_CONTENT_ID = "exoplayer_test_1"; - private static final String WIDEVINE_HW_SECURE_DECODE_CONTENT_ID = "exoplayer_test_2"; - private static final String WIDEVINE_SECURITY_LEVEL_1 = "L1"; - private static final String WIDEVINE_SECURITY_LEVEL_3 = "L3"; - private static final String SECURITY_LEVEL_PROPERTY = "securityLevel"; - - private final String streamName; - private final String manifestUrl; - private final MetricsLogger metricsLogger; - private final boolean fullPlaybackNoSeeking; - private final boolean isCddLimitedRetry; - private final DashTestTrackSelector trackSelector; - private final byte[] offlineLicenseKeySetId; - private final String widevineLicenseUrl; - private final boolean useL1Widevine; - - boolean needsCddLimitedRetry; - - public static String getWidevineLicenseUrl(boolean useL1Widevine) { - return WIDEVINE_LICENSE_URL - + (useL1Widevine ? WIDEVINE_HW_SECURE_DECODE_CONTENT_ID : WIDEVINE_SW_CRYPTO_CONTENT_ID); - } - - @TargetApi(18) - @SuppressWarnings("ResourceType") - public static boolean isL1WidevineAvailable(String mimeType) { - try { - // Force L3 if secure decoder is not available. - if (MediaCodecUtil.getDecoderInfo(mimeType, true) == null) { - return false; - } - MediaDrm mediaDrm = new MediaDrm(WIDEVINE_UUID); - String securityProperty = mediaDrm.getPropertyString(SECURITY_LEVEL_PROPERTY); - mediaDrm.release(); - return WIDEVINE_SECURITY_LEVEL_1.equals(securityProperty); - } catch (MediaCodecUtil.DecoderQueryException | UnsupportedSchemeException e) { - throw new IllegalStateException(e); - } - } - - /** - * @param tag A tag to use for logging. - * @param streamName The name of the test stream for metric logging. - * @param manifestUrl The manifest url. - * @param metricsLogger Logger to log metrics from the test. - * @param fullPlaybackNoSeeking Whether the test will play the entire source with no seeking. - * @param audioFormat The audio format. - * @param canIncludeAdditionalVideoFormats Whether to use video formats in addition to those - * listed in the videoFormats argument, if the device is capable of playing them. - * @param isCddLimitedRetry Whether this is a CDD limited retry following a previous failure. - * @param actionSchedule The action schedule for the test. - * @param offlineLicenseKeySetId The key set id of the license to be used. - * @param widevineLicenseUrl If the video is Widevine encrypted, this is the license url - * otherwise null. - * @param useL1Widevine Whether to use L1 Widevine. - * @param videoFormats The video formats. - */ - private DashHostedTest(String tag, String streamName, String manifestUrl, - MetricsLogger metricsLogger, boolean fullPlaybackNoSeeking, String audioFormat, - boolean canIncludeAdditionalVideoFormats, boolean isCddLimitedRetry, - ActionSchedule actionSchedule, byte[] offlineLicenseKeySetId, String widevineLicenseUrl, - boolean useL1Widevine, String... videoFormats) { - super(tag, fullPlaybackNoSeeking); - Assertions.checkArgument(!(isCddLimitedRetry && canIncludeAdditionalVideoFormats)); - this.streamName = streamName; - this.manifestUrl = manifestUrl; - this.metricsLogger = metricsLogger; - this.fullPlaybackNoSeeking = fullPlaybackNoSeeking; - this.isCddLimitedRetry = isCddLimitedRetry; - this.offlineLicenseKeySetId = offlineLicenseKeySetId; - this.widevineLicenseUrl = widevineLicenseUrl; - this.useL1Widevine = useL1Widevine; - trackSelector = new DashTestTrackSelector(tag, audioFormat, videoFormats, - canIncludeAdditionalVideoFormats); - if (actionSchedule != null) { - setSchedule(actionSchedule); - } - } - - @Override - protected MappingTrackSelector buildTrackSelector(HostActivity host, - BandwidthMeter bandwidthMeter) { - return trackSelector; - } - - @Override - protected DefaultDrmSessionManager buildDrmSessionManager( - final String userAgent) { - if (widevineLicenseUrl == null) { - return null; - } - try { - MediaDrmCallback drmCallback = new HttpMediaDrmCallback(widevineLicenseUrl, - new DefaultHttpDataSourceFactory(userAgent)); - DefaultDrmSessionManager drmSessionManager = - DefaultDrmSessionManager.newWidevineInstance(drmCallback, null, null, null); - if (!useL1Widevine) { - drmSessionManager.setPropertyString( - SECURITY_LEVEL_PROPERTY, WIDEVINE_SECURITY_LEVEL_3); - } - if (offlineLicenseKeySetId != null) { - drmSessionManager.setMode(DefaultDrmSessionManager.MODE_PLAYBACK, - offlineLicenseKeySetId); - } - return drmSessionManager; - } catch (UnsupportedDrmException e) { - throw new IllegalStateException(e); - } - } - - @Override - protected SimpleExoPlayer buildExoPlayer(HostActivity host, Surface surface, - MappingTrackSelector trackSelector, - DrmSessionManager drmSessionManager) { - SimpleExoPlayer player = new DebugSimpleExoPlayer(host, trackSelector, - new DefaultLoadControl(), drmSessionManager); - player.setVideoSurface(surface); - return player; - } - - @Override - protected MediaSource buildSource(HostActivity host, String userAgent, - TransferListener mediaTransferListener) { - DataSource.Factory manifestDataSourceFactory = new DefaultDataSourceFactory(host, userAgent); - DataSource.Factory mediaDataSourceFactory = new DefaultDataSourceFactory(host, userAgent, - mediaTransferListener); - Uri manifestUri = Uri.parse(manifestUrl); - DefaultDashChunkSource.Factory chunkSourceFactory = new DefaultDashChunkSource.Factory( - mediaDataSourceFactory); - return new DashMediaSource(manifestUri, manifestDataSourceFactory, chunkSourceFactory, - MIN_LOADABLE_RETRY_COUNT, 0 /* livePresentationDelayMs */, null, null); - } - - @Override - protected void logMetrics(DecoderCounters audioCounters, DecoderCounters videoCounters) { - metricsLogger.logMetric(MetricsLogger.KEY_TEST_NAME, streamName); - metricsLogger.logMetric(MetricsLogger.KEY_IS_CDD_LIMITED_RETRY, isCddLimitedRetry); - metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_DROPPED_COUNT, - videoCounters.droppedOutputBufferCount); - metricsLogger.logMetric(MetricsLogger.KEY_MAX_CONSECUTIVE_FRAMES_DROPPED_COUNT, - videoCounters.maxConsecutiveDroppedOutputBufferCount); - metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_SKIPPED_COUNT, - videoCounters.skippedOutputBufferCount); - metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_RENDERED_COUNT, - videoCounters.renderedOutputBufferCount); - metricsLogger.close(); - } - - @Override - protected void assertPassed(DecoderCounters audioCounters, DecoderCounters videoCounters) { - if (fullPlaybackNoSeeking) { - // We shouldn't have skipped any output buffers. - DecoderCountersUtil.assertSkippedOutputBufferCount(tag + AUDIO_TAG_SUFFIX, audioCounters, 0); - DecoderCountersUtil.assertSkippedOutputBufferCount(tag + VIDEO_TAG_SUFFIX, videoCounters, 0); - // We allow one fewer output buffer due to the way that MediaCodecRenderer and the - // underlying decoders handle the end of stream. This should be tightened up in the future. - DecoderCountersUtil.assertTotalOutputBufferCount(tag + AUDIO_TAG_SUFFIX, audioCounters, - audioCounters.inputBufferCount - 1, audioCounters.inputBufferCount); - DecoderCountersUtil.assertTotalOutputBufferCount(tag + VIDEO_TAG_SUFFIX, videoCounters, - videoCounters.inputBufferCount - 1, videoCounters.inputBufferCount); - } - try { - int droppedFrameLimit = (int) Math.ceil(MAX_DROPPED_VIDEO_FRAME_FRACTION - * DecoderCountersUtil.getTotalOutputBuffers(videoCounters)); - // Assert that performance is acceptable. - // Assert that total dropped frames were within limit. - DecoderCountersUtil.assertDroppedOutputBufferLimit(tag + VIDEO_TAG_SUFFIX, videoCounters, - droppedFrameLimit); - // Assert that consecutive dropped frames were within limit. - DecoderCountersUtil.assertConsecutiveDroppedOutputBufferLimit(tag + VIDEO_TAG_SUFFIX, - videoCounters, MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES); - } catch (AssertionFailedError e) { - if (trackSelector.includedAdditionalVideoFormats) { - // Retry limiting to CDD mandated formats (b/28220076). - Log.e(tag, "Too many dropped or consecutive dropped frames.", e); - needsCddLimitedRetry = true; - } else { - throw e; - } - } - } - - private static final class DashTestTrackSelector extends MappingTrackSelector { - - private final String tag; - private final String audioFormatId; - private final String[] videoFormatIds; - private final boolean canIncludeAdditionalVideoFormats; - - public boolean includedAdditionalVideoFormats; - - private DashTestTrackSelector(String tag, String audioFormatId, String[] videoFormatIds, - boolean canIncludeAdditionalVideoFormats) { - this.tag = tag; - this.audioFormatId = audioFormatId; - this.videoFormatIds = videoFormatIds; - this.canIncludeAdditionalVideoFormats = canIncludeAdditionalVideoFormats; - } - - @Override - protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, - TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) - throws ExoPlaybackException { - Assertions.checkState(rendererCapabilities[VIDEO_RENDERER_INDEX].getTrackType() - == C.TRACK_TYPE_VIDEO); - Assertions.checkState(rendererCapabilities[AUDIO_RENDERER_INDEX].getTrackType() - == C.TRACK_TYPE_AUDIO); - Assertions.checkState(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].length == 1); - Assertions.checkState(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].length == 1); - TrackSelection[] selections = new TrackSelection[rendererCapabilities.length]; - selections[VIDEO_RENDERER_INDEX] = new RandomTrackSelection( - rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0), - getVideoTrackIndices(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0), - rendererFormatSupports[VIDEO_RENDERER_INDEX][0], videoFormatIds, - canIncludeAdditionalVideoFormats), - 0 /* seed */); - selections[AUDIO_RENDERER_INDEX] = new FixedTrackSelection( - rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0), - getTrackIndex(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0), audioFormatId)); - includedAdditionalVideoFormats = - selections[VIDEO_RENDERER_INDEX].length() > videoFormatIds.length; - return selections; - } - - private int[] getVideoTrackIndices(TrackGroup trackGroup, int[] formatSupport, - String[] formatIds, boolean canIncludeAdditionalFormats) { - List trackIndices = new ArrayList<>(); - - // Always select explicitly listed representations. - for (String formatId : formatIds) { - int trackIndex = getTrackIndex(trackGroup, formatId); - Log.d(tag, "Adding base video format: " - + Format.toLogString(trackGroup.getFormat(trackIndex))); - trackIndices.add(trackIndex); - } - - // Select additional video representations, if supported by the device. - if (canIncludeAdditionalFormats) { - for (int i = 0; i < trackGroup.length; i++) { - if (!trackIndices.contains(i) && isFormatHandled(formatSupport[i])) { - Log.d(tag, "Adding extra video format: " - + Format.toLogString(trackGroup.getFormat(i))); - trackIndices.add(i); - } - } - } - - int[] trackIndicesArray = Util.toArray(trackIndices); - Arrays.sort(trackIndicesArray); - return trackIndicesArray; - } - - private static int getTrackIndex(TrackGroup trackGroup, String formatId) { - for (int i = 0; i < trackGroup.length; i++) { - if (trackGroup.getFormat(i).id.equals(formatId)) { - return i; - } - } - throw new IllegalStateException("Format " + formatId + " not found."); - } - - private static boolean isFormatHandled(int formatSupport) { - return (formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK) - == RendererCapabilities.FORMAT_HANDLED; - } - - } - -} diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java index c41cc1b0b7..fc0701da8d 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java @@ -40,40 +40,54 @@ public final class DashTest extends ActivityInstrumentationTestCase2= 24; + + private static final String AUDIO_TAG_SUFFIX = ":Audio"; + private static final String VIDEO_TAG_SUFFIX = ":Video"; + + private static final int MIN_LOADABLE_RETRY_COUNT = 10; + private static final int MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES = 10; + private static final float MAX_DROPPED_VIDEO_FRAME_FRACTION = 0.01f; + + private static final String WIDEVINE_SECURITY_LEVEL_1 = "L1"; + private static final String WIDEVINE_SECURITY_LEVEL_3 = "L3"; + private static final String SECURITY_LEVEL_PROPERTY = "securityLevel"; + + private final String tag; + private final HostActivity activity; + private final Instrumentation instrumentation; + + private String streamName; + private boolean fullPlaybackNoSeeking; + private String audioFormat; + private boolean canIncludeAdditionalVideoFormats; + private ActionSchedule actionSchedule; + private byte[] offlineLicenseKeySetId; + private String[] videoFormats; + private String manifestUrl; + private boolean useL1Widevine; + private String widevineLicenseUrl; + private DataSource.Factory dataSourceFactory; + + @TargetApi(18) + @SuppressWarnings("ResourceType") + public static boolean isL1WidevineAvailable(String mimeType) { + try { + // Force L3 if secure decoder is not available. + if (MediaCodecUtil.getDecoderInfo(mimeType, true) == null) { + return false; + } + MediaDrm mediaDrm = new MediaDrm(WIDEVINE_UUID); + String securityProperty = mediaDrm.getPropertyString(SECURITY_LEVEL_PROPERTY); + mediaDrm.release(); + return WIDEVINE_SECURITY_LEVEL_1.equals(securityProperty); + } catch (MediaCodecUtil.DecoderQueryException | UnsupportedSchemeException e) { + throw new IllegalStateException(e); + } + } + + public DashTestRunner(String tag, HostActivity activity, Instrumentation instrumentation) { + this.tag = tag; + this.activity = activity; + this.instrumentation = instrumentation; + } + + public DashTestRunner setStreamName(String streamName) { + this.streamName = streamName; + return this; + } + + public DashTestRunner setFullPlaybackNoSeeking(boolean fullPlaybackNoSeeking) { + this.fullPlaybackNoSeeking = fullPlaybackNoSeeking; + return this; + } + + public DashTestRunner setCanIncludeAdditionalVideoFormats( + boolean canIncludeAdditionalVideoFormats) { + this.canIncludeAdditionalVideoFormats = canIncludeAdditionalVideoFormats + && ALLOW_ADDITIONAL_VIDEO_FORMATS; + return this; + } + + public DashTestRunner setActionSchedule(ActionSchedule actionSchedule) { + this.actionSchedule = actionSchedule; + return this; + } + + public DashTestRunner setOfflineLicenseKeySetId(byte[] offlineLicenseKeySetId) { + this.offlineLicenseKeySetId = offlineLicenseKeySetId; + return this; + } + + public DashTestRunner setAudioVideoFormats(String audioFormat, String... videoFormats) { + this.audioFormat = audioFormat; + this.videoFormats = videoFormats; + return this; + } + + public DashTestRunner setManifestUrl(String manifestUrl) { + this.manifestUrl = manifestUrl; + return this; + } + + public DashTestRunner setWidevineMimeType(String mimeType) { + this.useL1Widevine = isL1WidevineAvailable(mimeType); + this.widevineLicenseUrl = DashTestData.getWidevineLicenseUrl(useL1Widevine); + return this; + } + + public DashTestRunner setDataSourceFactory(DataSource.Factory dataSourceFactory) { + this.dataSourceFactory = dataSourceFactory; + return this; + } + + public void run() { + DashHostedTest test = createDashHostedTest(canIncludeAdditionalVideoFormats, false, + instrumentation); + activity.runTest(test, TEST_TIMEOUT_MS); + // Retry test exactly once if adaptive test fails due to excessive dropped buffers when + // playing non-CDD required formats (b/28220076). + if (test.needsCddLimitedRetry) { + activity.runTest(createDashHostedTest(false, true, instrumentation), TEST_TIMEOUT_MS); + } + } + + private DashHostedTest createDashHostedTest(boolean canIncludeAdditionalVideoFormats, + boolean isCddLimitedRetry, Instrumentation instrumentation) { + MetricsLogger metricsLogger = MetricsLogger.Factory.createDefault(instrumentation, tag, + REPORT_NAME, REPORT_OBJECT_NAME); + return new DashHostedTest(tag, streamName, manifestUrl, metricsLogger, fullPlaybackNoSeeking, + audioFormat, canIncludeAdditionalVideoFormats, isCddLimitedRetry, actionSchedule, + offlineLicenseKeySetId, widevineLicenseUrl, useL1Widevine, dataSourceFactory, + videoFormats); + } + + /** + * A {@link HostedTest} for DASH playback tests. + */ + @TargetApi(16) + private static final class DashHostedTest extends ExoHostedTest { + + private final String streamName; + private final String manifestUrl; + private final MetricsLogger metricsLogger; + private final boolean fullPlaybackNoSeeking; + private final boolean isCddLimitedRetry; + private final DashTestTrackSelector trackSelector; + private final byte[] offlineLicenseKeySetId; + private final String widevineLicenseUrl; + private final boolean useL1Widevine; + private final DataSource.Factory dataSourceFactory; + + private boolean needsCddLimitedRetry; + + /** + * @param tag A tag to use for logging. + * @param streamName The name of the test stream for metric logging. + * @param manifestUrl The manifest url. + * @param metricsLogger Logger to log metrics from the test. + * @param fullPlaybackNoSeeking Whether the test will play the entire source with no seeking. + * @param audioFormat The audio format. + * @param canIncludeAdditionalVideoFormats Whether to use video formats in addition to those + * listed in the videoFormats argument, if the device is capable of playing them. + * @param isCddLimitedRetry Whether this is a CDD limited retry following a previous failure. + * @param actionSchedule The action schedule for the test. + * @param offlineLicenseKeySetId The key set id of the license to be used. + * @param widevineLicenseUrl If the video is Widevine encrypted, this is the license url + * otherwise null. + * @param useL1Widevine Whether to use L1 Widevine. + * @param dataSourceFactory If not null, used to load manifest and media. + * @param videoFormats The video formats. + */ + private DashHostedTest(String tag, String streamName, String manifestUrl, + MetricsLogger metricsLogger, boolean fullPlaybackNoSeeking, String audioFormat, + boolean canIncludeAdditionalVideoFormats, boolean isCddLimitedRetry, + ActionSchedule actionSchedule, byte[] offlineLicenseKeySetId, String widevineLicenseUrl, + boolean useL1Widevine, DataSource.Factory dataSourceFactory, String... videoFormats) { + super(tag, fullPlaybackNoSeeking); + Assertions.checkArgument(!(isCddLimitedRetry && canIncludeAdditionalVideoFormats)); + this.streamName = streamName; + this.manifestUrl = manifestUrl; + this.metricsLogger = metricsLogger; + this.fullPlaybackNoSeeking = fullPlaybackNoSeeking; + this.isCddLimitedRetry = isCddLimitedRetry; + this.offlineLicenseKeySetId = offlineLicenseKeySetId; + this.widevineLicenseUrl = widevineLicenseUrl; + this.useL1Widevine = useL1Widevine; + this.dataSourceFactory = dataSourceFactory; + trackSelector = new DashTestTrackSelector(tag, audioFormat, videoFormats, + canIncludeAdditionalVideoFormats); + if (actionSchedule != null) { + setSchedule(actionSchedule); + } + } + + @Override + protected MappingTrackSelector buildTrackSelector(HostActivity host, + BandwidthMeter bandwidthMeter) { + return trackSelector; + } + + @Override + protected DefaultDrmSessionManager buildDrmSessionManager( + final String userAgent) { + if (widevineLicenseUrl == null) { + return null; + } + try { + MediaDrmCallback drmCallback = new HttpMediaDrmCallback(widevineLicenseUrl, + new DefaultHttpDataSourceFactory(userAgent)); + DefaultDrmSessionManager drmSessionManager = + DefaultDrmSessionManager.newWidevineInstance(drmCallback, null, null, null); + if (!useL1Widevine) { + drmSessionManager.setPropertyString( + SECURITY_LEVEL_PROPERTY, WIDEVINE_SECURITY_LEVEL_3); + } + if (offlineLicenseKeySetId != null) { + drmSessionManager.setMode(DefaultDrmSessionManager.MODE_PLAYBACK, + offlineLicenseKeySetId); + } + return drmSessionManager; + } catch (UnsupportedDrmException e) { + throw new IllegalStateException(e); + } + } + + @Override + protected SimpleExoPlayer buildExoPlayer(HostActivity host, Surface surface, + MappingTrackSelector trackSelector, + DrmSessionManager drmSessionManager) { + SimpleExoPlayer player = new DebugSimpleExoPlayer(host, trackSelector, + new DefaultLoadControl(), drmSessionManager); + player.setVideoSurface(surface); + return player; + } + + @Override + protected MediaSource buildSource(HostActivity host, String userAgent, + TransferListener mediaTransferListener) { + DataSource.Factory manifestDataSourceFactory = dataSourceFactory != null + ? dataSourceFactory : new DefaultDataSourceFactory(host, userAgent); + DataSource.Factory mediaDataSourceFactory = dataSourceFactory != null + ? dataSourceFactory + : new DefaultDataSourceFactory(host, userAgent, mediaTransferListener); + Uri manifestUri = Uri.parse(manifestUrl); + DefaultDashChunkSource.Factory chunkSourceFactory = new DefaultDashChunkSource.Factory( + mediaDataSourceFactory); + return new DashMediaSource(manifestUri, manifestDataSourceFactory, chunkSourceFactory, + MIN_LOADABLE_RETRY_COUNT, 0 /* livePresentationDelayMs */, null, null); + } + + @Override + protected void logMetrics(DecoderCounters audioCounters, DecoderCounters videoCounters) { + metricsLogger.logMetric(MetricsLogger.KEY_TEST_NAME, streamName); + metricsLogger.logMetric(MetricsLogger.KEY_IS_CDD_LIMITED_RETRY, isCddLimitedRetry); + metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_DROPPED_COUNT, + videoCounters.droppedOutputBufferCount); + metricsLogger.logMetric(MetricsLogger.KEY_MAX_CONSECUTIVE_FRAMES_DROPPED_COUNT, + videoCounters.maxConsecutiveDroppedOutputBufferCount); + metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_SKIPPED_COUNT, + videoCounters.skippedOutputBufferCount); + metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_RENDERED_COUNT, + videoCounters.renderedOutputBufferCount); + metricsLogger.close(); + } + + @Override + protected void assertPassed(DecoderCounters audioCounters, DecoderCounters videoCounters) { + if (fullPlaybackNoSeeking) { + // We shouldn't have skipped any output buffers. + DecoderCountersUtil + .assertSkippedOutputBufferCount(tag + AUDIO_TAG_SUFFIX, audioCounters, 0); + DecoderCountersUtil + .assertSkippedOutputBufferCount(tag + VIDEO_TAG_SUFFIX, videoCounters, 0); + // We allow one fewer output buffer due to the way that MediaCodecRenderer and the + // underlying decoders handle the end of stream. This should be tightened up in the future. + DecoderCountersUtil.assertTotalOutputBufferCount(tag + AUDIO_TAG_SUFFIX, audioCounters, + audioCounters.inputBufferCount - 1, audioCounters.inputBufferCount); + DecoderCountersUtil.assertTotalOutputBufferCount(tag + VIDEO_TAG_SUFFIX, videoCounters, + videoCounters.inputBufferCount - 1, videoCounters.inputBufferCount); + } + try { + int droppedFrameLimit = (int) Math.ceil(MAX_DROPPED_VIDEO_FRAME_FRACTION + * DecoderCountersUtil.getTotalOutputBuffers(videoCounters)); + // Assert that performance is acceptable. + // Assert that total dropped frames were within limit. + DecoderCountersUtil.assertDroppedOutputBufferLimit(tag + VIDEO_TAG_SUFFIX, videoCounters, + droppedFrameLimit); + // Assert that consecutive dropped frames were within limit. + DecoderCountersUtil.assertConsecutiveDroppedOutputBufferLimit(tag + VIDEO_TAG_SUFFIX, + videoCounters, MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES); + } catch (AssertionFailedError e) { + if (trackSelector.includedAdditionalVideoFormats) { + // Retry limiting to CDD mandated formats (b/28220076). + Log.e(tag, "Too many dropped or consecutive dropped frames.", e); + needsCddLimitedRetry = true; + } else { + throw e; + } + } + } + + } + + private static final class DashTestTrackSelector extends MappingTrackSelector { + + private final String tag; + private final String audioFormatId; + private final String[] videoFormatIds; + private final boolean canIncludeAdditionalVideoFormats; + + public boolean includedAdditionalVideoFormats; + + private DashTestTrackSelector(String tag, String audioFormatId, String[] videoFormatIds, + boolean canIncludeAdditionalVideoFormats) { + this.tag = tag; + this.audioFormatId = audioFormatId; + this.videoFormatIds = videoFormatIds; + this.canIncludeAdditionalVideoFormats = canIncludeAdditionalVideoFormats; + } + + @Override + protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, + TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) + throws ExoPlaybackException { + Assertions.checkState(rendererCapabilities[VIDEO_RENDERER_INDEX].getTrackType() + == C.TRACK_TYPE_VIDEO); + Assertions.checkState(rendererCapabilities[AUDIO_RENDERER_INDEX].getTrackType() + == C.TRACK_TYPE_AUDIO); + Assertions.checkState(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].length == 1); + Assertions.checkState(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].length == 1); + TrackSelection[] selections = new TrackSelection[rendererCapabilities.length]; + selections[VIDEO_RENDERER_INDEX] = new RandomTrackSelection( + rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0), + getVideoTrackIndices(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0), + rendererFormatSupports[VIDEO_RENDERER_INDEX][0], videoFormatIds, + canIncludeAdditionalVideoFormats), + 0 /* seed */); + selections[AUDIO_RENDERER_INDEX] = new FixedTrackSelection( + rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0), + getTrackIndex(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0), audioFormatId)); + includedAdditionalVideoFormats = + selections[VIDEO_RENDERER_INDEX].length() > videoFormatIds.length; + return selections; + } + + private int[] getVideoTrackIndices(TrackGroup trackGroup, int[] formatSupport, + String[] formatIds, boolean canIncludeAdditionalFormats) { + List trackIndices = new ArrayList<>(); + + // Always select explicitly listed representations. + for (String formatId : formatIds) { + int trackIndex = getTrackIndex(trackGroup, formatId); + Log.d(tag, "Adding base video format: " + + Format.toLogString(trackGroup.getFormat(trackIndex))); + trackIndices.add(trackIndex); + } + + // Select additional video representations, if supported by the device. + if (canIncludeAdditionalFormats) { + for (int i = 0; i < trackGroup.length; i++) { + if (!trackIndices.contains(i) && isFormatHandled(formatSupport[i])) { + Log.d(tag, "Adding extra video format: " + + Format.toLogString(trackGroup.getFormat(i))); + trackIndices.add(i); + } + } + } + + int[] trackIndicesArray = Util.toArray(trackIndices); + Arrays.sort(trackIndicesArray); + return trackIndicesArray; + } + + private static int getTrackIndex(TrackGroup trackGroup, String formatId) { + for (int i = 0; i < trackGroup.length; i++) { + if (trackGroup.getFormat(i).id.equals(formatId)) { + return i; + } + } + throw new IllegalStateException("Format " + formatId + " not found."); + } + + private static boolean isFormatHandled(int formatSupport) { + return (formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK) + == RendererCapabilities.FORMAT_HANDLED; + } + + } + +} diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java index dc173ce881..99a6f3bef5 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java @@ -37,7 +37,7 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa private static final String TAG = "DashWidevineOfflineTest"; private static final String USER_AGENT = "ExoPlayerPlaybackTests"; - private DashHostedTest.Builder builder; + private DashTestRunner testRunner; private DefaultHttpDataSourceFactory httpDataSourceFactory; private OfflineLicenseHelper offlineLicenseHelper; private byte[] offlineLicenseKeySetId; @@ -49,7 +49,7 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa @Override protected void setUp() throws Exception { super.setUp(); - builder = new DashHostedTest.Builder(TAG) + testRunner = new DashTestRunner(TAG, getActivity(), getInstrumentation()) .setStreamName("test_widevine_h264_fixed_offline") .setManifestUrl(DashTestData.WIDEVINE_H264_MANIFEST) .setWidevineMimeType(MimeTypes.VIDEO_H264) @@ -58,8 +58,8 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa .setAudioVideoFormats(DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, DashTestData.WIDEVINE_H264_CDD_FIXED); - boolean useL1Widevine = DashHostedTest.isL1WidevineAvailable(MimeTypes.VIDEO_H264); - String widevineLicenseUrl = DashHostedTest.getWidevineLicenseUrl(useL1Widevine); + boolean useL1Widevine = DashTestRunner.isL1WidevineAvailable(MimeTypes.VIDEO_H264); + String widevineLicenseUrl = DashTestData.getWidevineLicenseUrl(useL1Widevine); httpDataSourceFactory = new DefaultHttpDataSourceFactory(USER_AGENT); offlineLicenseHelper = OfflineLicenseHelper.newWidevineInstance(widevineLicenseUrl, httpDataSourceFactory); @@ -67,12 +67,15 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa @Override protected void tearDown() throws Exception { + testRunner = null; if (offlineLicenseKeySetId != null) { releaseLicense(); } if (offlineLicenseHelper != null) { offlineLicenseHelper.releaseResources(); } + offlineLicenseHelper = null; + httpDataSourceFactory = null; super.tearDown(); } @@ -83,7 +86,7 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa return; // Pass. } downloadLicense(); - builder.runTest(getActivity(), getInstrumentation()); + testRunner.run(); // Renew license after playback should still work offlineLicenseKeySetId = offlineLicenseHelper.renew(offlineLicenseKeySetId); @@ -98,7 +101,7 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa releaseLicense(); // keySetId no longer valid. try { - builder.runTest(getActivity(), getInstrumentation()); + testRunner.run(); fail("Playback should fail because the license has been released."); } catch (Throwable e) { // Get the root cause @@ -138,7 +141,7 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa } // DefaultDrmSessionManager should renew the license and stream play fine - builder.runTest(getActivity(), getInstrumentation()); + testRunner.run(); } public void testWidevineOfflineLicenseExpiresOnPause() throws Exception { @@ -157,9 +160,7 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa .delay(3000).pause().delay(licenseDuration * 1000 + 2000).play().build(); // DefaultDrmSessionManager should renew the license and stream play fine - builder - .setActionSchedule(schedule) - .runTest(getActivity(), getInstrumentation()); + testRunner.setActionSchedule(schedule).run(); } private void downloadLicense() throws InterruptedException, DrmSessionException, IOException { @@ -167,7 +168,7 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa httpDataSourceFactory.createDataSource(), DashTestData.WIDEVINE_H264_MANIFEST); Assert.assertNotNull(offlineLicenseKeySetId); Assert.assertTrue(offlineLicenseKeySetId.length > 0); - builder.setOfflineLicenseKeySetId(offlineLicenseKeySetId); + testRunner.setOfflineLicenseKeySetId(offlineLicenseKeySetId); } private void releaseLicense() throws DrmSessionException { From b98de975f18712de850ac43d742b248699e95b16 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 16 Mar 2017 05:53:58 -0700 Subject: [PATCH 139/140] Make Util.inferContentType marginally smarter Issue: #2513 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150310349 --- .../exoplayer2/demo/PlayerActivity.java | 4 ++-- .../google/android/exoplayer2/util/Util.java | 22 ++++++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index ffe6bb1fee..adb04eaa24 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -316,8 +316,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay } private MediaSource buildMediaSource(Uri uri, String overrideExtension) { - int type = Util.inferContentType(!TextUtils.isEmpty(overrideExtension) ? "." + overrideExtension - : uri.getLastPathSegment()); + int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) + : Util.inferContentType("." + overrideExtension); switch (type) { case C.TYPE_SS: return new SsMediaSource(uri, buildDataSourceFactory(false), diff --git a/library/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/src/main/java/com/google/android/exoplayer2/util/Util.java index 60846170b9..d9282700d7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -790,6 +790,18 @@ public final class Util { } } + /** + * Makes a best guess to infer the type from a {@link Uri}. + * + * @param uri The {@link Uri}. + * @return The content type. + */ + @C.ContentType + public static int inferContentType(Uri uri) { + String path = uri.getPath(); + return path == null ? C.TYPE_OTHER : inferContentType(path); + } + /** * Makes a best guess to infer the type from a file name. * @@ -798,14 +810,14 @@ public final class Util { */ @C.ContentType public static int inferContentType(String fileName) { - if (fileName == null) { - return C.TYPE_OTHER; - } else if (fileName.endsWith(".mpd")) { + fileName = fileName.toLowerCase(); + if (fileName.endsWith(".mpd")) { return C.TYPE_DASH; - } else if (fileName.endsWith(".ism") || fileName.endsWith(".isml")) { - return C.TYPE_SS; } else if (fileName.endsWith(".m3u8")) { return C.TYPE_HLS; + } else if (fileName.endsWith(".ism") || fileName.endsWith(".isml") + || fileName.endsWith(".ism/manifest") || fileName.endsWith(".isml/manifest")) { + return C.TYPE_SS; } else { return C.TYPE_OTHER; } From c8059e5969372113f3249455e32c85d2ee6fbf49 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 16 Mar 2017 13:08:58 -0700 Subject: [PATCH 140/140] Update release notes + bump versions ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=150358346 --- RELEASENOTES.md | 53 +++++++++++++++++++ build.gradle | 2 +- demo/src/main/AndroidManifest.xml | 4 +- .../exoplayer2/ExoPlayerLibraryInfo.java | 4 +- 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 234c91daba..f45cb9aff6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,52 @@ # Release notes # +### r2.3.0 ### + +* GVR extension: Wraps the Google VR Audio SDK to provide spatial audio + rendering. You can read more about the GVR extension + [here](https://medium.com/google-exoplayer/spatial-audio-with-exoplayer-and-gvr-cecb00e9da5f#.xdjebjd7g). +* DASH improvements: + * Support embedded CEA-608 closed captions + ([#2362](https://github.com/google/ExoPlayer/issues/2362)). + * Support embedded EMSG events + ([#2176](https://github.com/google/ExoPlayer/issues/2176)). + * Support mspr:pro manifest element + ([#2386](https://github.com/google/ExoPlayer/issues/2386)). + * Correct handling of empty segment indices at the start of live events + ([#1865](https://github.com/google/ExoPlayer/issues/1865)). +* HLS improvements: + * Respect initial track selection + ([#2353](https://github.com/google/ExoPlayer/issues/2353)). + * Reduced frequency of media playlist requests when playback position is close + to the live edge ([#2548](https://github.com/google/ExoPlayer/issues/2548)). + * Exposed the master playlist through ExoPlayer.getCurrentManifest() + ([#2537](https://github.com/google/ExoPlayer/issues/2537)). + * Support CLOSED-CAPTIONS #EXT-X-MEDIA type + ([#341](https://github.com/google/ExoPlayer/issues/341)). + * Fixed handling of negative values in #EXT-X-SUPPORT + ([#2495](https://github.com/google/ExoPlayer/issues/2495)). + * Fixed potential endless buffering state for streams with WebVTT subtitles + ([#2424](https://github.com/google/ExoPlayer/issues/2424)). +* MPEG-TS improvements: + * Support for multiple programs. + * Support for multiple closed captions and caption service descriptors + ([#2161](https://github.com/google/ExoPlayer/issues/2161)). +* MP3: Add `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` extractor option to enable + constant bitrate seeking in MP3 files that would otherwise be unseekable + ([#2445](https://github.com/google/ExoPlayer/issues/2445)). +* ID3: Better handle malformed ID3 data + ([#2486](https://github.com/google/ExoPlayer/issues/2486)). +* Track selection: Added maxVideoBitrate parameter to DefaultTrackSelector. +* DRM: Add support for CENC ClearKey on API level 21+ + ([#2361](https://github.com/google/ExoPlayer/issues/2361)). +* DRM: Support dynamic setting of key request headers + ([#1924](https://github.com/google/ExoPlayer/issues/1924)). +* SmoothStreaming: Fixed handling of start_time placeholder + ([#2447](https://github.com/google/ExoPlayer/issues/2447)). +* FLAC extension: Fix proguard configuration + ([#2427](https://github.com/google/ExoPlayer/issues/2427)). +* Misc bugfixes. + ### r2.2.0 ### * Demo app: Automatic recovery from BehindLiveWindowException, plus improved @@ -246,6 +293,12 @@ in all V2 releases. This cannot be assumed for changes in r1.5.12 and later, however it can be assumed that all such changes are included in the most recent V2 release. +### r1.5.15 ### + +* SmoothStreaming: Fixed handling of start_time placeholder + ([#2447](https://github.com/google/ExoPlayer/issues/2447)). +* Misc bugfixes. + ### r1.5.14 ### * Fixed cache failures when using an encrypted cache content index. diff --git a/build.gradle b/build.gradle index fabac36293..f1901a1270 100644 --- a/build.gradle +++ b/build.gradle @@ -37,7 +37,7 @@ allprojects { releaseRepoName = getBintrayRepo() releaseUserOrg = 'google' releaseGroupId = 'com.google.android.exoplayer' - releaseVersion = 'r2.2.0' + releaseVersion = 'r2.3.0' releaseWebsite = 'https://github.com/google/ExoPlayer' } } diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 2f3dc0d1bf..a834c5df19 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2300" + android:versionName="2.3.0"> diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 5100acbbd8..5ec7fac5dd 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo { /** * The version of the library, expressed as a string. */ - String VERSION = "2.2.0"; + String VERSION = "2.3.0"; /** * The version of the library, expressed as an integer. @@ -32,7 +32,7 @@ public interface ExoPlayerLibraryInfo { * corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding * integer version 123045006 (123-045-006). */ - int VERSION_INT = 2002000; + int VERSION_INT = 2003000; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}