From 6ff02ccfb1c14293d59cf0f166194f53ac489cc2 Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Sun, 14 Feb 2021 14:10:43 +0200 Subject: [PATCH 01/34] add ImageTrack entity --- playkit/build.gradle | 2 +- .../java/com/kaltura/playkit/PlayerEvent.java | 14 ++ .../playkit/player/ExoPlayerWrapper.java | 9 +- .../kaltura/playkit/player/ImageTrack.java | 120 ++++++++++++++++++ .../playkit/player/MediaPlayerWrapper.java | 4 +- .../com/kaltura/playkit/player/PKTracks.java | 33 ++++- .../playkit/player/PlayerController.java | 7 + .../kaltura/playkit/player/ThumbnailInfo.java | 37 ++++++ .../playkit/player/TrackSelectionHelper.java | 78 +++++++++++- .../com/kaltura/playkit/utils/Consts.java | 4 + 10 files changed, 297 insertions(+), 11 deletions(-) create mode 100644 playkit/src/main/java/com/kaltura/playkit/player/ImageTrack.java create mode 100644 playkit/src/main/java/com/kaltura/playkit/player/ThumbnailInfo.java diff --git a/playkit/build.gradle b/playkit/build.gradle index c8ac0c62c..9f86eafae 100644 --- a/playkit/build.gradle +++ b/playkit/build.gradle @@ -35,7 +35,7 @@ tasks.withType(Javadoc) { dependencies { - def exoPlayerVersion = '2.13.0' + def exoPlayerVersion = '2.13.100' api "com.kaltura.playkit:kexoplayer:$exoPlayerVersion" api 'com.google.code.gson:gson:2.8.5' diff --git a/playkit/src/main/java/com/kaltura/playkit/PlayerEvent.java b/playkit/src/main/java/com/kaltura/playkit/PlayerEvent.java index 0026a4e0e..3c01927d8 100644 --- a/playkit/src/main/java/com/kaltura/playkit/PlayerEvent.java +++ b/playkit/src/main/java/com/kaltura/playkit/PlayerEvent.java @@ -15,6 +15,7 @@ import androidx.annotation.NonNull; import com.kaltura.playkit.player.AudioTrack; +import com.kaltura.playkit.player.ImageTrack; import com.kaltura.playkit.player.PKAspectRatioResizeMode; import com.kaltura.playkit.player.PKTracks; import com.kaltura.playkit.player.TextTrack; @@ -40,6 +41,8 @@ public class PlayerEvent implements PKEvent { public static final Class videoTrackChanged = VideoTrackChanged.class; public static final Class audioTrackChanged = AudioTrackChanged.class; public static final Class textTrackChanged = TextTrackChanged.class; + public static final Class imageTrackChanged = ImageTrackChanged.class; + public static final Class playbackRateChanged = PlaybackRateChanged.class; public static final Class subtitlesStyleChanged = SubtitlesStyleChanged.class; public static final Class videoFramesDropped = VideoFramesDropped.class; @@ -207,6 +210,16 @@ public TextTrackChanged(TextTrack newTrack) { } } + public static class ImageTrackChanged extends PlayerEvent { + + public final ImageTrack newTrack; + + public ImageTrackChanged(ImageTrack newTrack) { + super(Type.IMAGE_TRACK_CHANGED); + this.newTrack = newTrack; + } + } + public static class PlaybackRateChanged extends PlayerEvent { public final float rate; @@ -354,6 +367,7 @@ public enum Type { VIDEO_TRACK_CHANGED, AUDIO_TRACK_CHANGED, TEXT_TRACK_CHANGED, + IMAGE_TRACK_CHANGED, PLAYBACK_RATE_CHANGED, CONNECTION_ACQUIRED, VIDEO_FRAMES_DROPPED, // Video frames were dropped, see PlayerEvent.VideoFramesDropped diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java index 29cb12580..d058a181b 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java @@ -136,7 +136,7 @@ public interface LoadControlStrategy { private float lastKnownPlaybackRate = Consts.DEFAULT_PLAYBACK_RATE_SPEED; private List metadataList = new ArrayList<>(); - private String[] lastSelectedTrackIds = {TrackSelectionHelper.NONE, TrackSelectionHelper.NONE, TrackSelectionHelper.NONE}; + private String[] lastSelectedTrackIds = {TrackSelectionHelper.NONE, TrackSelectionHelper.NONE, TrackSelectionHelper.NONE, TrackSelectionHelper.NONE}; private TrackSelectionHelper.TracksInfoListener tracksInfoListener = initTracksInfoListener(); private TrackSelectionHelper.TracksErrorListener tracksErrorListener = initTracksErrorListener(); @@ -1297,7 +1297,7 @@ public void stop() { preferredLanguageWasSelected = false; lastKnownVolume = Consts.DEFAULT_VOLUME; lastKnownPlaybackRate = Consts.DEFAULT_PLAYBACK_RATE_SPEED; - lastSelectedTrackIds = new String[]{TrackSelectionHelper.NONE, TrackSelectionHelper.NONE, TrackSelectionHelper.NONE}; + lastSelectedTrackIds = new String[]{TrackSelectionHelper.NONE, TrackSelectionHelper.NONE, TrackSelectionHelper.NONE, TrackSelectionHelper.NONE}; if (assertTrackSelectionIsNotNull("stop()")) { trackSelectionHelper.stop(); } @@ -1427,6 +1427,11 @@ public void onAudioTrackChanged() { public void onTextTrackChanged() { sendEvent(PlayerEvent.Type.TEXT_TRACK_CHANGED); } + + @Override + public void onImageTrackChanged() { + sendEvent(PlayerEvent.Type.IMAGE_TRACK_CHANGED); + } }; } diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ImageTrack.java b/playkit/src/main/java/com/kaltura/playkit/player/ImageTrack.java new file mode 100644 index 000000000..8cbe36b3f --- /dev/null +++ b/playkit/src/main/java/com/kaltura/playkit/player/ImageTrack.java @@ -0,0 +1,120 @@ +/* + * ============================================================================ + * Copyright (C) 2017 Kaltura Inc. + * + * Licensed under the AGPLv3 license, unless a different license for a + * particular library is specified in the applicable library path. + * + * You may obtain a copy of the License at + * https://www.gnu.org/licenses/agpl-3.0.html + * ============================================================================ + */ + +package com.kaltura.playkit.player; + +import androidx.annotation.Nullable; + +import com.kaltura.playkit.PKAudioCodec; + +/** + * Image track data holder. + * + */ +public class ImageTrack extends BaseTrack { + + + private String label; + private long bitrate; + private String structure; + private int tilesHorizontal; + private int tilesVertical; + private int width; + private int height; + private long segmentDuration; + private long presentationTimeOffset; + private long timeScale; + private long startNumber; + private long endNumber; + private String imageTemplateUrl; + + ImageTrack(String uniqueId, + String label, + long bitrate, + int width, + int height, + int tilesHorizontal, + int tilesVertical, + long segmentDuration, + long startNumber, + long endtNumber, + long presentationTimeOffset, + long timeScale, + String imageTemplateUrl + ) { + super(uniqueId, 0, false); + this.label = label; + this.bitrate = bitrate; + this.width = width; + this.height = height; + this.tilesHorizontal = tilesHorizontal; + this.tilesVertical = tilesVertical; + this.segmentDuration = segmentDuration; + this.startNumber = startNumber; + this.endNumber = endtNumber; + this.presentationTimeOffset = presentationTimeOffset; + this.timeScale = timeScale; + this.imageTemplateUrl = imageTemplateUrl; + } + + public String getLabel() { + return label; + } + + public long getBitrate() { + return bitrate; + } + + public String getStructure() { + return structure; + } + + public int getTilesHorizontal() { + return tilesHorizontal; + } + + public int getTilesVertical() { + return tilesVertical; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public long getSegmentDuration() { + return segmentDuration; + } + + public long getPresentationTimeOffset() { + return presentationTimeOffset; + } + + public long getTimeScale() { + return timeScale; + } + + public long getStartNumber() { + return startNumber; + } + + public long getEndNumber() { + return endNumber; + } + + public String getImageTemplateUrl() { + return imageTemplateUrl; + } +} diff --git a/playkit/src/main/java/com/kaltura/playkit/player/MediaPlayerWrapper.java b/playkit/src/main/java/com/kaltura/playkit/player/MediaPlayerWrapper.java index 2e589240b..79428abdf 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/MediaPlayerWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/MediaPlayerWrapper.java @@ -289,8 +289,8 @@ public float getVolume() { @Override public PKTracks getPKTracks() { - return new PKTracks(new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), - 0, 0, 0); + return new PKTracks(new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), + 0, 0, 0, 0); } @Override diff --git a/playkit/src/main/java/com/kaltura/playkit/player/PKTracks.java b/playkit/src/main/java/com/kaltura/playkit/player/PKTracks.java index 1f97c1194..1ac6f8b10 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/PKTracks.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/PKTracks.java @@ -26,21 +26,26 @@ public class PKTracks { protected int defaultVideoTrackIndex; protected int defaultAudioTrackIndex; protected int defaultTextTrackIndex; + protected int defaultImageTrackIndex; + private List videoTracks; private List audioTracks; private List textTracks; + private List imageTracks; - public PKTracks(List videoTracks, List audioTracks, List textTracks, - int defaultVideoTrackIndex, int defaultAudioTrackIndex, int defaultTextTrackIndex) { + public PKTracks(List videoTracks, List audioTracks, List textTracks, List imageTracks, + int defaultVideoTrackIndex, int defaultAudioTrackIndex, int defaultTextTrackIndex, int defaultImageTrackIndex) { this.audioTracks = audioTracks; this.videoTracks = videoTracks; this.textTracks = textTracks; + this.imageTracks = imageTracks; this.defaultVideoTrackIndex = defaultVideoTrackIndex; this.defaultAudioTrackIndex = defaultAudioTrackIndex; this.defaultTextTrackIndex = defaultTextTrackIndex; + this.defaultImageTrackIndex = defaultImageTrackIndex; } /** @@ -82,6 +87,19 @@ public List getTextTracks() { return textTracks; } + /** + * Getter for imageTracks list. + * Before use, the list entry's should be casted to {@link ImageTrack} in order to receive the + * full track info of that type. + * Can be empty, if no tracks available. + * + * @return - the list of all available Image tracks, that can be used for image preview feature. + */ + @NonNull + public List getImageTracks() { + return imageTracks; + } + /** * Getter for default video track index. * The one that will be selected by player based on the media manifest. @@ -114,5 +132,16 @@ public int getDefaultAudioTrackIndex() { public int getDefaultTextTrackIndex() { return defaultTextTrackIndex; } + + /** + * Getter for default image track index. + * The one that will be selected by player based on the media manifest. + * If no default available in the manifest, the index will be 0. + * + * @return - the index of the track that is set by default. + */ + public int getDefaultImageTrackIndex() { + return defaultImageTrackIndex; + } } diff --git a/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java b/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java index 0080b16ec..9e55ee030 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java @@ -855,6 +855,13 @@ private PlayerEngine.EventListener initEventListener() { } event = new PlayerEvent.TextTrackChanged(textTrack); break; + case IMAGE_TRACK_CHANGED: + ImageTrack imageTrack = (ImageTrack) player.getLastSelectedTrack(Consts.TRACK_TYPE_IMAGE); + if (imageTrack == null) { + return; + } + event = new PlayerEvent.ImageTrackChanged(imageTrack); + break; case PLAYBACK_RATE_CHANGED: event = new PlayerEvent.PlaybackRateChanged(player.getPlaybackRate()); break; diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ThumbnailInfo.java b/playkit/src/main/java/com/kaltura/playkit/player/ThumbnailInfo.java new file mode 100644 index 000000000..105c7514e --- /dev/null +++ b/playkit/src/main/java/com/kaltura/playkit/player/ThumbnailInfo.java @@ -0,0 +1,37 @@ +package com.kaltura.playkit.player; + +public class ThumbnailInfo { + private String url; // url of the image that contains the thumbnail slice + private int x; // x position of the thumbnail + private int y; // y position of the thumbnail + private int width; // width of the thumbnail + private int height; // height of the thumbnail + + public ThumbnailInfo(String url, int x, int y, int width, int height) { + this.url = url; + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + public String getUrl() { + return url; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } +} \ No newline at end of file diff --git a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java index 2bcbd4efb..f75e5c6bc 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java @@ -53,6 +53,7 @@ import static com.kaltura.android.exoplayer2.util.MimeTypes.VIDEO_VP8; import static com.kaltura.android.exoplayer2.util.MimeTypes.VIDEO_VP9; import static com.kaltura.playkit.utils.Consts.TRACK_TYPE_AUDIO; +import static com.kaltura.playkit.utils.Consts.TRACK_TYPE_IMAGE; import static com.kaltura.playkit.utils.Consts.TRACK_TYPE_TEXT; import static com.kaltura.playkit.utils.Consts.TRACK_TYPE_UNKNOWN; import static com.kaltura.playkit.utils.Consts.TRACK_TYPE_VIDEO; @@ -80,6 +81,7 @@ class TrackSelectionHelper { private static final String VIDEO_PREFIX = "Video:"; private static final String AUDIO_PREFIX = "Audio:"; private static final String TEXT_PREFIX = "Text:"; + private static final String IMAGE_PREFIX = "Image:"; private static final String CEA_608 = "application/cea-608"; private static final String LANGUAGE_UNKNOWN = "Unknown"; @@ -92,6 +94,8 @@ class TrackSelectionHelper { private List videoTracks = new ArrayList<>(); private List audioTracks = new ArrayList<>(); private List textTracks = new ArrayList<>(); + private List imageTracks = new ArrayList<>(); + private Map>> subtitleListMap = new HashMap<>(); private Map> videoTracksCodecsMap = new HashMap<>(); @@ -123,6 +127,8 @@ interface TracksInfoListener { void onAudioTrackChanged(); void onTextTrackChanged(); + + void onImageTrackChanged(); } interface TracksErrorListener { @@ -286,6 +292,32 @@ private PKTracks buildTracks() { } } + if (mappedTrackInfo != null) { + TrackGroupArray imageTrackGroupArray = mappedTrackInfo.getUnmappedTrackGroups(); + for (int imageTrackIndex = 0 ; imageTrackIndex < imageTrackGroupArray.length; imageTrackIndex++) { + TrackGroup imageTrackGroup = imageTrackGroupArray.get(imageTrackIndex); + for (int trackIndex = 0; trackIndex < imageTrackGroup.length; trackIndex++) { + Format imageFormat = imageTrackGroup.getFormat(trackIndex); + Format.FormatThumbnailInfo formatThumbnailInfo = imageFormat.formatThumbnailInfo; + String uniqueId = getUniqueId(TRACK_TYPE_IMAGE, TRACK_TYPE_IMAGE, trackIndex); + imageTracks.add(trackIndex, new ImageTrack(uniqueId, + imageFormat.id, + imageFormat.bitrate, + imageFormat.width, + imageFormat.height, + formatThumbnailInfo.tilesHorizontal, + formatThumbnailInfo.tilesVertical, + formatThumbnailInfo.segmentDuration, + formatThumbnailInfo.startNumber, + formatThumbnailInfo.endNumber, + formatThumbnailInfo.presentationTimeOffset, + formatThumbnailInfo.timeScale, + formatThumbnailInfo.imageTemplateUrl + )); + } + } + } + //add disable option to the text tracks. maybeAddDisabledTextTrack(); videoTracks = filterVideoTracks(); @@ -295,8 +327,10 @@ private PKTracks buildTracks() { int defaultVideoTrackIndex = getDefaultTrackIndex(videoTracks, lastSelectedTrackIds[TRACK_TYPE_VIDEO]); int defaultAudioTrackIndex = getDefaultTrackIndex(filteredAudioTracks, lastSelectedTrackIds[TRACK_TYPE_AUDIO]); int defaultTextTrackIndex = getDefaultTrackIndex(textTracks, lastSelectedTrackIds[TRACK_TYPE_TEXT]); + int defaultImageTrackIndex = getDefaultTrackIndex(imageTracks, lastSelectedTrackIds[TRACK_TYPE_IMAGE]); + Collections.sort(videoTracks); - return new PKTracks(videoTracks, filteredAudioTracks, textTracks, defaultVideoTrackIndex, defaultAudioTrackIndex, defaultTextTrackIndex); + return new PKTracks(videoTracks, filteredAudioTracks, textTracks, imageTracks, defaultVideoTrackIndex, defaultAudioTrackIndex, defaultTextTrackIndex, defaultImageTrackIndex); } private boolean checkTracksUnavailability(MappingTrackSelector.MappedTrackInfo mappedTrackInfo) { @@ -524,6 +558,10 @@ private int getDefaultTrackIndex(List trackList, String las return defaultTrackIndex; } + if (trackList.get(0) instanceof ImageTrack) { + return restoreLastSelectedTrack(trackList, lastSelectedTrackId, getUpdatedDefaultTrackIndex(trackList, defaultTrackIndex)); + } + for (int i = 0; i < trackList.size(); i++) { if (trackList.get(i) != null) { int selectionFlag = trackList.get(i).getSelectionFlag(); @@ -713,6 +751,8 @@ private String getUniqueIdPrefix(int rendererIndex) { return AUDIO_PREFIX; case TRACK_TYPE_TEXT: return TEXT_PREFIX; + case TRACK_TYPE_IMAGE: + return IMAGE_PREFIX; default: return ""; } @@ -744,11 +784,19 @@ protected void changeTrack(String uniqueId) { return; } + int[] uniqueTrackId = validateUniqueId(uniqueId); int rendererIndex = uniqueTrackId[RENDERER_INDEX]; requestedChangeTrackIds[rendererIndex] = uniqueId; + if (uniqueId.contains(IMAGE_PREFIX)) { + log.d("Image track changed to: " + requestedChangeTrackIds[TRACK_TYPE_IMAGE]); + lastSelectedTrackIds[TRACK_TYPE_IMAGE] = requestedChangeTrackIds[TRACK_TYPE_IMAGE]; + tracksInfoListener.onImageTrackChanged(); + return; + } + DefaultTrackSelector.ParametersBuilder parametersBuilder = selector.getParameters().buildUpon(); if (rendererIndex == TRACK_TYPE_TEXT) { //Disable text track renderer if needed. @@ -1196,6 +1244,7 @@ private int[] validateUniqueId(String uniqueId) throws IllegalArgumentException if (uniqueId.contains(VIDEO_PREFIX) || uniqueId.contains(AUDIO_PREFIX) || uniqueId.contains(TEXT_PREFIX) + || uniqueId.contains(IMAGE_PREFIX) && uniqueId.contains(",")) { int[] parsedUniqueId = parseUniqueId(uniqueId); @@ -1220,6 +1269,10 @@ private boolean isTrackIndexValid(int[] parsedUniqueId) { int groupIndex = parsedUniqueId[GROUP_INDEX]; int trackIndex = parsedUniqueId[TRACK_INDEX]; + if (rendererIndex == TRACK_TYPE_IMAGE) { + return trackIndex >= TRACK_ADAPTIVE; + } + if (rendererIndex == TRACK_TYPE_TEXT) { return trackIndex != TRACK_ADAPTIVE && trackIndex >= TRACK_DISABLED @@ -1231,12 +1284,16 @@ private boolean isTrackIndexValid(int[] parsedUniqueId) { } private boolean isGroupIndexValid(int[] parsedUniqueId) { + if (parsedUniqueId[GROUP_INDEX] == TRACK_TYPE_IMAGE) { + return true; + } + return parsedUniqueId[GROUP_INDEX] >= 0 && parsedUniqueId[GROUP_INDEX] < mappedTrackInfo.getTrackGroups(parsedUniqueId[RENDERER_INDEX]).length; } private boolean isRendererTypeValid(int rendererIndex) { - return rendererIndex >= TRACK_TYPE_VIDEO && rendererIndex <= TRACK_TYPE_TEXT; + return rendererIndex >= TRACK_TYPE_VIDEO && rendererIndex <= TRACK_TYPE_IMAGE; } /** @@ -1377,6 +1434,12 @@ protected void notifyAboutTrackChange(TrackSelectionArray trackSelections) { lastSelectedTrackIds[TRACK_TYPE_TEXT] = requestedChangeTrackIds[TRACK_TYPE_TEXT]; tracksInfoListener.onTextTrackChanged(); } + + if (shouldNotifyAboutTrackChanged(TRACK_TYPE_IMAGE)) { + log.d("Image track changed to: " + requestedChangeTrackIds[TRACK_TYPE_IMAGE]); + lastSelectedTrackIds[TRACK_TYPE_IMAGE] = requestedChangeTrackIds[TRACK_TYPE_IMAGE]; + tracksInfoListener.onImageTrackChanged(); + } } private boolean shouldNotifyAboutTrackChanged(int renderType) { @@ -1407,6 +1470,13 @@ BaseTrack getLastSelectedTrack(int renderType) { } } break; + case TRACK_TYPE_IMAGE: + for (ImageTrack track : imageTracks) { + if (track.getUniqueId().equals(lastSelectedTrackIds[renderType])) { + return track; + } + } + break; } log.w("For some reason we could not found lastSelectedTrack of the specified render type = " + renderType); @@ -1415,8 +1485,8 @@ BaseTrack getLastSelectedTrack(int renderType) { // clean previous selection protected void stop() { - lastSelectedTrackIds = new String[]{NONE, NONE, NONE}; - requestedChangeTrackIds = new String[]{NONE, NONE, NONE}; + lastSelectedTrackIds = new String[]{NONE, NONE, NONE, NONE}; + requestedChangeTrackIds = new String[]{NONE, NONE, NONE, NONE}; trackSelectionArray = null; mappedTrackInfo = null; videoTracks.clear(); diff --git a/playkit/src/main/java/com/kaltura/playkit/utils/Consts.java b/playkit/src/main/java/com/kaltura/playkit/utils/Consts.java index 1e5260d3b..1cadf7046 100644 --- a/playkit/src/main/java/com/kaltura/playkit/utils/Consts.java +++ b/playkit/src/main/java/com/kaltura/playkit/utils/Consts.java @@ -48,6 +48,10 @@ public class Consts { * Identifier for the Text track type. */ public static final int TRACK_TYPE_TEXT = 2; + /** + * Identifier for the Image track type. + */ + public static final int TRACK_TYPE_IMAGE = 3; /** * Identifier for the unknown track type. */ From b7dfdaab735e542dc7364d5472d8a06899ea3643 Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Sun, 14 Feb 2021 19:58:24 +0200 Subject: [PATCH 02/34] add getThumbnailInfo API --- .../main/java/com/kaltura/playkit/Player.java | 8 +++++ .../kaltura/playkit/PlayerDecoratorBase.java | 6 ++++ .../kaltura/playkit/PlayerEngineWrapper.java | 6 ++++ .../playkit/player/ExoPlayerWrapper.java | 7 +++- .../playkit/player/PlayerController.java | 8 +++++ .../kaltura/playkit/player/PlayerEngine.java | 2 ++ .../playkit/player/TrackSelectionHelper.java | 33 ++++++++++++++++++- 7 files changed, 68 insertions(+), 2 deletions(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/Player.java b/playkit/src/main/java/com/kaltura/playkit/Player.java index 490e24fab..011b5754b 100644 --- a/playkit/src/main/java/com/kaltura/playkit/Player.java +++ b/playkit/src/main/java/com/kaltura/playkit/Player.java @@ -21,6 +21,7 @@ import com.kaltura.playkit.player.PKMaxVideoSize; import com.kaltura.playkit.player.PlayerView; import com.kaltura.playkit.player.SubtitleStyleSettings; +import com.kaltura.playkit.player.ThumbnailInfo; import com.kaltura.playkit.player.VideoCodecSettings; import com.kaltura.playkit.player.AudioCodecSettings; import com.kaltura.playkit.player.vr.VRSettings; @@ -519,6 +520,13 @@ interface Settings { */ float getPlaybackRate(); + /** + * get the Information for a thumbnailImage by position + * + * @param positionMS - relevant image for given player position. + */ + ThumbnailInfo getThumbnailInfo(long positionMS); + /** * Generic getters for playkit controllers. * diff --git a/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java b/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java index 5c1d311b1..1abcafb87 100644 --- a/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java +++ b/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java @@ -18,6 +18,7 @@ import com.kaltura.playkit.player.PKAspectRatioResizeMode; import com.kaltura.playkit.player.PlayerView; import com.kaltura.playkit.player.SubtitleStyleSettings; +import com.kaltura.playkit.player.ThumbnailInfo; import java.util.List; @@ -93,6 +94,11 @@ public float getPlaybackRate() { return player.getPlaybackRate(); } + @Override + public ThumbnailInfo getThumbnailInfo(long positionMS) { + return player.getThumbnailInfo(positionMS); + } + @Override public void play() { player.play(); diff --git a/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java b/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java index 27248d65d..3901526fa 100644 --- a/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java @@ -8,6 +8,7 @@ import com.kaltura.playkit.player.PlayerView; import com.kaltura.playkit.player.Profiler; import com.kaltura.playkit.player.SubtitleStyleSettings; +import com.kaltura.playkit.player.ThumbnailInfo; import com.kaltura.playkit.player.metadata.PKMetadata; import java.util.List; @@ -191,6 +192,11 @@ public float getPlaybackRate() { return playerEngine.getPlaybackRate(); } + @Override + public ThumbnailInfo getThumbnailInfo(long positionMS) { + return playerEngine.getThumbnailInfo(positionMS); + } + @Override public void setProfiler(Profiler profiler) { this.playerEngine.setProfiler(profiler); diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java index d058a181b..312010b2c 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java @@ -1466,6 +1466,11 @@ public boolean isLive() { return false; } + @Override + public ThumbnailInfo getThumbnailInfo(long positionMS) { + return trackSelectionHelper.getThumbnailInfo(positionMS); + } + private void closeProfilerSession() { profiler.onSessionFinished(); } @@ -1566,7 +1571,7 @@ public void updateSurfaceAspectRatioResizeMode(PKAspectRatioResizeMode resizeMod configureAspectRatioResizeMode(); sendEvent(PlayerEvent.Type.ASPECT_RATIO_RESIZE_MODE_CHANGED); } - + private void configureAspectRatioResizeMode() { if(exoPlayerView != null){ exoPlayerView.setSurfaceAspectRatioResizeMode(playerSettings.getAspectRatioResizeMode()); diff --git a/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java b/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java index 9e55ee030..2fa8e5a43 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java @@ -661,6 +661,14 @@ public float getPlaybackRate() { return Consts.PLAYBACK_SPEED_RATE_UNKNOWN; } + @Override + public ThumbnailInfo getThumbnailInfo(long positionMS) { + log.v("getThumbnailInfo"); + if (assertPlayerIsNotNull("getThumbnailInfo()")) { + return player.getThumbnailInfo(positionMS); + } + return null; + } @Override public void updateSubtitleStyle(SubtitleStyleSettings subtitleStyleSettings) { diff --git a/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java b/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java index db869c795..1f5f3978b 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java @@ -286,6 +286,8 @@ default void updateSurfaceAspectRatioResizeMode(PKAspectRatioResizeMode resizeMo */ void onOrientationChanged(); + default ThumbnailInfo getThumbnailInfo(long positionMS) { return null; } + interface EventListener { void onEvent(PlayerEvent.Type event); } diff --git a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java index f75e5c6bc..236961ea2 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java @@ -307,7 +307,7 @@ private PKTracks buildTracks() { imageFormat.height, formatThumbnailInfo.tilesHorizontal, formatThumbnailInfo.tilesVertical, - formatThumbnailInfo.segmentDuration, + formatThumbnailInfo.segmentDuration * Consts.MILLISECONDS_MULTIPLIER, formatThumbnailInfo.startNumber, formatThumbnailInfo.endNumber, formatThumbnailInfo.presentationTimeOffset, @@ -1157,6 +1157,37 @@ public void updateTrackSelectorParameter(PlayerSettings playerSettings, DefaultT } } + public ThumbnailInfo getThumbnailInfo(long positionMS) { + if (imageTracks.isEmpty()) { + return null; + } + + ImageTrack imageTrack = null; + for (int index = 0; index < imageTracks.size() ; index++) { + if (imageTracks.get(index).getUniqueId().equals(lastSelectedTrackIds[TRACK_TYPE_IMAGE])) { + imageTrack = imageTracks.get(index); + break; + } + } + + long seq = (long)Math.floor(positionMS / imageTrack.getSegmentDuration()); + double offset = positionMS % imageTrack.getSegmentDuration(); + int thumbIndex = (int) Math.floor((offset * imageTrack.getTilesHorizontal() * imageTrack.getTilesVertical()) / imageTrack.getSegmentDuration()); + long seqIdx = seq + imageTrack.getStartNumber(); + int imageWidth = (int) Math.floor(imageTrack.getWidth() / imageTrack.getTilesHorizontal()); + int imageHeight = (int) Math.floor(imageTrack.getHeight() / imageTrack.getTilesVertical()); + int imageX = (int) Math.floor(thumbIndex % imageTrack.getTilesHorizontal()) * imageWidth; + int imageY = (int) Math.floor(thumbIndex / imageTrack.getTilesHorizontal()) * imageHeight; + + long imageRealUrlTime = ((seqIdx - 1) * imageTrack.getSegmentDuration()); + String realImageUrl = imageTrack.getImageTemplateUrl().replace("$Number$", String.valueOf(seqIdx)).replace("$Time$", String.valueOf(imageRealUrlTime)); + //int imageXColIndex = imageX / imageWidth; + //int imageYRowIndex = imageY / imageHeight; + //Rect rect = new Rect(imageX, imageY, imageXColIndex * imageWidth + imageWidth, imageYRowIndex * imageHeight + imageHeight); + return new ThumbnailInfo(realImageUrl, imageX, imageY, imageWidth, imageHeight); + } + + /** * Checks if adaptive track for the specified group was created. * From 6b20280043e87c86665e923fb76b038cbb26bd06 Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Mon, 15 Feb 2021 11:38:01 +0200 Subject: [PATCH 03/34] fox position in case of live stream --- .../java/com/kaltura/playkit/player/ExoPlayerWrapper.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java index 312010b2c..a161ca6fa 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java @@ -1468,6 +1468,12 @@ public boolean isLive() { @Override public ThumbnailInfo getThumbnailInfo(long positionMS) { + if(isLive()) { + Timeline timeline = player.getCurrentTimeline(); + if (!timeline.isEmpty()) { + positionMS -= timeline.getPeriod(player.getCurrentPeriodIndex(), new Timeline.Period()).getPositionInWindowMs(); + } + } return trackSelectionHelper.getThumbnailInfo(positionMS); } @@ -1571,7 +1577,7 @@ public void updateSurfaceAspectRatioResizeMode(PKAspectRatioResizeMode resizeMod configureAspectRatioResizeMode(); sendEvent(PlayerEvent.Type.ASPECT_RATIO_RESIZE_MODE_CHANGED); } - + private void configureAspectRatioResizeMode() { if(exoPlayerView != null){ exoPlayerView.setSurfaceAspectRatioResizeMode(playerSettings.getAspectRatioResizeMode()); From 34201e497d05b9fb7ee34313b8520484b95eb11d Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Sun, 21 Feb 2021 14:00:19 +0200 Subject: [PATCH 04/34] standalone manifest parser --- .../CustomAdaptationSet.java | 71 + .../CustomDashManifest.java | 237 ++ .../CustomDashManifestParser.java | 1948 +++++++++++++++++ .../dashmanifestparser/CustomFormat.java | 1801 +++++++++++++++ .../dashmanifestparser/CustomPeriod.java | 96 + .../CustomRepresentation.java | 362 +++ .../dashmanifestparser/CustomSegmentBase.java | 563 +++++ .../CustomSingleSegmentIndex.java | 72 + .../playkit/player/ExoPlayerWrapper.java | 40 +- .../playkit/player/TrackSelectionHelper.java | 3 +- 10 files changed, 5187 insertions(+), 6 deletions(-) create mode 100644 playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomAdaptationSet.java create mode 100644 playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomDashManifest.java create mode 100644 playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomDashManifestParser.java create mode 100644 playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomFormat.java create mode 100644 playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomPeriod.java create mode 100644 playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomRepresentation.java create mode 100644 playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomSegmentBase.java create mode 100644 playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomSingleSegmentIndex.java diff --git a/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomAdaptationSet.java b/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomAdaptationSet.java new file mode 100644 index 000000000..d210ecdc0 --- /dev/null +++ b/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomAdaptationSet.java @@ -0,0 +1,71 @@ +package com.kaltura.android.exoplayer2.dashmanifestparser; + +import com.kaltura.android.exoplayer2.source.dash.manifest.Descriptor; +import com.kaltura.android.exoplayer2.source.dash.manifest.Representation; + +import java.util.Collections; +import java.util.List; + +/** + * Represents a set of interchangeable encoded versions of a media content component. + */ +public class CustomAdaptationSet { + + /** + * Value of {@link #id} indicating no value is set.= + */ + public static final int ID_UNSET = -1; + + /** + * A non-negative identifier for the adaptation set that's unique in the scope of its containing + * period, or {@link #ID_UNSET} if not specified. + */ + public final int id; + + /** + * The type of the adaptation set. One of the {@link com.kaltura.android.exoplayer2.C} + * {@code TRACK_TYPE_*} constants. + */ + public final int type; + + /** + * {@link Representation}s in the adaptation set. + */ + public final List representations; + + /** + * Accessibility descriptors in the adaptation set. + */ + public final List accessibilityDescriptors; + + /** Essential properties in the adaptation set. */ + public final List essentialProperties; + + /** Supplemental properties in the adaptation set. */ + public final List supplementalProperties; + + /** + * @param id A non-negative identifier for the adaptation set that's unique in the scope of its + * containing period, or {@link #ID_UNSET} if not specified. + * @param type The type of the adaptation set. One of the {@link com.kaltura.android.exoplayer2.C} + * {@code TRACK_TYPE_*} constants. + * @param representations {@link Representation}s in the adaptation set. + * @param accessibilityDescriptors Accessibility descriptors in the adaptation set. + * @param essentialProperties Essential properties in the adaptation set. + * @param supplementalProperties Supplemental properties in the adaptation set. + */ + public CustomAdaptationSet( + int id, + int type, + List representations, + List accessibilityDescriptors, + List essentialProperties, + List supplementalProperties) { + this.id = id; + this.type = type; + this.representations = Collections.unmodifiableList(representations); + this.accessibilityDescriptors = Collections.unmodifiableList(accessibilityDescriptors); + this.essentialProperties = Collections.unmodifiableList(essentialProperties); + this.supplementalProperties = Collections.unmodifiableList(supplementalProperties); + } +} diff --git a/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomDashManifest.java b/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomDashManifest.java new file mode 100644 index 000000000..3e1d77b71 --- /dev/null +++ b/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomDashManifest.java @@ -0,0 +1,237 @@ +package com.kaltura.android.exoplayer2.dashmanifestparser; + +import android.net.Uri; +import androidx.annotation.Nullable; +import com.kaltura.android.exoplayer2.C; +import com.kaltura.android.exoplayer2.offline.FilterableManifest; +import com.kaltura.android.exoplayer2.offline.StreamKey; +import com.kaltura.android.exoplayer2.source.dash.manifest.ProgramInformation; +import com.kaltura.android.exoplayer2.source.dash.manifest.ServiceDescriptionElement; +import com.kaltura.android.exoplayer2.source.dash.manifest.UtcTimingElement; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + * Represents a DASH media presentation description (mpd), as defined by ISO/IEC 23009-1:2014 + * Section 5.3.1.2. + */ +public class CustomDashManifest implements FilterableManifest { + + /** + * The {@code availabilityStartTime} value in milliseconds since epoch, or {@link C#TIME_UNSET} if + * not present. + */ + public final long availabilityStartTimeMs; + + /** + * The duration of the presentation in milliseconds, or {@link C#TIME_UNSET} if not applicable. + */ + public final long durationMs; + + /** + * The {@code minBufferTime} value in milliseconds, or {@link C#TIME_UNSET} if not present. + */ + public final long minBufferTimeMs; + + /** + * Whether the manifest has value "dynamic" for the {@code type} attribute. + */ + public final boolean dynamic; + + /** + * The {@code minimumUpdatePeriod} value in milliseconds, or {@link C#TIME_UNSET} if not + * applicable. + */ + public final long minUpdatePeriodMs; + + /** + * The {@code timeShiftBufferDepth} value in milliseconds, or {@link C#TIME_UNSET} if not + * present. + */ + public final long timeShiftBufferDepthMs; + + /** + * The {@code suggestedPresentationDelay} value in milliseconds, or {@link C#TIME_UNSET} if not + * present. + */ + public final long suggestedPresentationDelayMs; + + /** + * The {@code publishTime} value in milliseconds since epoch, or {@link C#TIME_UNSET} if + * not present. + */ + public final long publishTimeMs; + + /** + * The {@link UtcTimingElement}, or null if not present. Defined in DVB A168:7/2016, Section + * 4.7.2. + */ + @Nullable public final UtcTimingElement utcTiming; + + /** The {@link ServiceDescriptionElement}, or null if not present. */ + @Nullable public final ServiceDescriptionElement serviceDescription; + + /** The location of this manifest, or null if not present. */ + @Nullable public final Uri location; + + /** The {@link ProgramInformation}, or null if not present. */ + @Nullable public final ProgramInformation programInformation; + + private final List periods; + + /** + * @deprecated Use {@link #CustomDashManifest(long, long, long, boolean, long, long, long, long, + * ProgramInformation, UtcTimingElement, ServiceDescriptionElement, Uri, List)}. + */ + @Deprecated + public CustomDashManifest( + long availabilityStartTimeMs, + long durationMs, + long minBufferTimeMs, + boolean dynamic, + long minUpdatePeriodMs, + long timeShiftBufferDepthMs, + long suggestedPresentationDelayMs, + long publishTimeMs, + @Nullable UtcTimingElement utcTiming, + @Nullable Uri location, + List periods) { + this( + availabilityStartTimeMs, + durationMs, + minBufferTimeMs, + dynamic, + minUpdatePeriodMs, + timeShiftBufferDepthMs, + suggestedPresentationDelayMs, + publishTimeMs, + /* programInformation= */ null, + utcTiming, + /* serviceDescription= */ null, + location, + periods); + } + + public CustomDashManifest( + long availabilityStartTimeMs, + long durationMs, + long minBufferTimeMs, + boolean dynamic, + long minUpdatePeriodMs, + long timeShiftBufferDepthMs, + long suggestedPresentationDelayMs, + long publishTimeMs, + @Nullable ProgramInformation programInformation, + @Nullable UtcTimingElement utcTiming, + @Nullable ServiceDescriptionElement serviceDescription, + @Nullable Uri location, + List periods) { + this.availabilityStartTimeMs = availabilityStartTimeMs; + this.durationMs = durationMs; + this.minBufferTimeMs = minBufferTimeMs; + this.dynamic = dynamic; + this.minUpdatePeriodMs = minUpdatePeriodMs; + this.timeShiftBufferDepthMs = timeShiftBufferDepthMs; + this.suggestedPresentationDelayMs = suggestedPresentationDelayMs; + this.publishTimeMs = publishTimeMs; + this.programInformation = programInformation; + this.utcTiming = utcTiming; + this.location = location; + this.serviceDescription = serviceDescription; + this.periods = periods == null ? Collections.emptyList() : periods; + } + + public final int getPeriodCount() { + return periods.size(); + } + + public final CustomPeriod getPeriod(int index) { + return periods.get(index); + } + + public final long getPeriodDurationMs(int index) { + return index == periods.size() - 1 + ? (durationMs == C.TIME_UNSET ? C.TIME_UNSET : (durationMs - periods.get(index).startMs)) + : (periods.get(index + 1).startMs - periods.get(index).startMs); + } + + public final long getPeriodDurationUs(int index) { + return C.msToUs(getPeriodDurationMs(index)); + } + + @Override + public final CustomDashManifest copy(List streamKeys) { + LinkedList keys = new LinkedList<>(streamKeys); + Collections.sort(keys); + keys.add(new StreamKey(-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 { + CustomPeriod period = getPeriod(periodIndex); + ArrayList copyAdaptationSets = + copyAdaptationSets(period.adaptationSets, keys); + CustomPeriod copiedPeriod = new CustomPeriod(period.id, period.startMs - shiftMs, copyAdaptationSets, + period.eventStreams); + copyPeriods.add(copiedPeriod); + } + } + long newDuration = durationMs != C.TIME_UNSET ? durationMs - shiftMs : C.TIME_UNSET; + return new CustomDashManifest( + availabilityStartTimeMs, + newDuration, + minBufferTimeMs, + dynamic, + minUpdatePeriodMs, + timeShiftBufferDepthMs, + suggestedPresentationDelayMs, + publishTimeMs, + programInformation, + utcTiming, + serviceDescription, + location, + copyPeriods); + } + + private static ArrayList copyAdaptationSets( + List adaptationSets, LinkedList keys) { + StreamKey key = keys.poll(); + int periodIndex = key.periodIndex; + ArrayList copyAdaptationSets = new ArrayList<>(); + do { + int adaptationSetIndex = key.groupIndex; + CustomAdaptationSet adaptationSet = adaptationSets.get(adaptationSetIndex); + + List representations = adaptationSet.representations; + ArrayList copyRepresentations = new ArrayList<>(); + do { + CustomRepresentation representation = representations.get(key.trackIndex); + copyRepresentations.add(representation); + key = keys.poll(); + } while (key.periodIndex == periodIndex && key.groupIndex == adaptationSetIndex); + + copyAdaptationSets.add( + new CustomAdaptationSet( + adaptationSet.id, + adaptationSet.type, + copyRepresentations, + adaptationSet.accessibilityDescriptors, + adaptationSet.essentialProperties, + adaptationSet.supplementalProperties)); + } 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/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomDashManifestParser.java b/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomDashManifestParser.java new file mode 100644 index 000000000..7375fe218 --- /dev/null +++ b/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomDashManifestParser.java @@ -0,0 +1,1948 @@ +package com.kaltura.android.exoplayer2.dashmanifestparser; + +import android.net.Uri; +import android.os.Build; +import android.text.TextUtils; +import android.util.Base64; +import android.util.Pair; +import android.util.Xml; +import androidx.annotation.Nullable; +import com.kaltura.android.exoplayer2.C; +import com.kaltura.android.exoplayer2.Format; +import com.kaltura.android.exoplayer2.ParserException; +import com.kaltura.android.exoplayer2.drm.DrmInitData; +import com.kaltura.android.exoplayer2.drm.DrmInitData.SchemeData; +import com.kaltura.android.exoplayer2.extractor.mp4.PsshAtomUtil; +import com.kaltura.android.exoplayer2.metadata.emsg.EventMessage; +import com.kaltura.android.exoplayer2.source.dash.manifest.AdaptationSet; +import com.kaltura.android.exoplayer2.source.dash.manifest.Descriptor; +import com.kaltura.android.exoplayer2.source.dash.manifest.EventStream; +import com.kaltura.android.exoplayer2.source.dash.manifest.ProgramInformation; +import com.kaltura.android.exoplayer2.source.dash.manifest.RangedUri; +import com.kaltura.android.exoplayer2.source.dash.manifest.Representation; +import com.kaltura.android.exoplayer2.source.dash.manifest.ServiceDescriptionElement; +import com.kaltura.android.exoplayer2.source.dash.manifest.UrlTemplate; +import com.kaltura.android.exoplayer2.source.dash.manifest.UtcTimingElement; +import com.kaltura.android.exoplayer2.util.Assertions; +import com.kaltura.android.exoplayer2.util.Log; +import com.kaltura.android.exoplayer2.util.MimeTypes; +import com.kaltura.android.exoplayer2.util.UriUtil; +import com.kaltura.android.exoplayer2.util.Util; +import com.kaltura.android.exoplayer2.util.XmlPullParserUtil; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.xml.sax.helpers.DefaultHandler; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; +import org.xmlpull.v1.XmlSerializer; + +import static com.kaltura.android.exoplayer2.util.MimeTypes.BASE_TYPE_IMAGE; +import static com.kaltura.android.exoplayer2.dashmanifestparser.CustomSegmentBase.*; + +/** + * A parser of media presentation description files. + */ +public class CustomDashManifestParser extends DefaultHandler { + + private static final String TAG = "MpdParser"; + + private static final Pattern FRAME_RATE_PATTERN = Pattern.compile("(\\d+)(?:/(\\d+))?"); + + private static final Pattern CEA_608_ACCESSIBILITY_PATTERN = Pattern.compile("CC([1-4])=.*"); + private static final Pattern CEA_708_ACCESSIBILITY_PATTERN = + Pattern.compile("([1-9]|[1-5][0-9]|6[0-3])=.*"); + + /** + * Maps the value attribute of an AudioElementConfiguration with schemeIdUri + * "urn:mpeg:mpegB:cicp:ChannelConfiguration", as defined by ISO 23001-8 clause 8.1, to a channel + * count. + */ + private static final int[] MPEG_CHANNEL_CONFIGURATION_MAPPING = + new int[] { + Format.NO_VALUE, 1, 2, 3, 4, 5, 6, 8, 2, 3, 4, 7, 8, 24, 8, 12, 10, 12, 14, 12, 14 + }; + + private final XmlPullParserFactory xmlParserFactory; + + public CustomDashManifestParser() { + try { + xmlParserFactory = XmlPullParserFactory.newInstance(); + } catch (XmlPullParserException e) { + throw new RuntimeException("Couldn't create XmlPullParserFactory instance", e); + } + } + + // MPD parsing. + + public CustomDashManifest parse(Uri uri, String dashManifest) throws IOException { + try { + XmlPullParser xpp = xmlParserFactory.newPullParser(); + xpp.setInput( new StringReader( dashManifest ) ); + int eventType = xpp.next(); + if (eventType != XmlPullParser.START_TAG || !"MPD".equals(xpp.getName())) { + throw new ParserException( + "inputStream does not contain a valid media presentation description"); + } + return parseMediaPresentationDescription(xpp, uri.toString()); + } catch (XmlPullParserException e) { + throw new ParserException(e); + } + } + + protected CustomDashManifest parseMediaPresentationDescription(XmlPullParser xpp, + String baseUrl) throws XmlPullParserException, IOException { + long availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", C.TIME_UNSET); + long durationMs = parseDuration(xpp, "mediaPresentationDuration", C.TIME_UNSET); + long minBufferTimeMs = parseDuration(xpp, "minBufferTime", C.TIME_UNSET); + String typeString = xpp.getAttributeValue(null, "type"); + boolean dynamic = "dynamic".equals(typeString); + long minUpdateTimeMs = dynamic ? parseDuration(xpp, "minimumUpdatePeriod", C.TIME_UNSET) + : C.TIME_UNSET; + long timeShiftBufferDepthMs = dynamic + ? parseDuration(xpp, "timeShiftBufferDepth", C.TIME_UNSET) : C.TIME_UNSET; + long suggestedPresentationDelayMs = dynamic + ? parseDuration(xpp, "suggestedPresentationDelay", C.TIME_UNSET) : C.TIME_UNSET; + long publishTimeMs = parseDateTime(xpp, "publishTime", C.TIME_UNSET); + ProgramInformation programInformation = null; + UtcTimingElement utcTiming = null; + Uri location = null; + ServiceDescriptionElement serviceDescription = null; + long baseUrlAvailabilityTimeOffsetUs = dynamic ? 0 : C.TIME_UNSET; + + List periods = new ArrayList<>(); + long nextPeriodStartMs = dynamic ? C.TIME_UNSET : 0; + boolean seenEarlyAccessPeriod = false; + boolean seenFirstBaseUrl = false; + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "BaseURL")) { + if (!seenFirstBaseUrl) { + baseUrlAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, baseUrlAvailabilityTimeOffsetUs); + baseUrl = parseBaseUrl(xpp, baseUrl); + seenFirstBaseUrl = true; + } + } else if (XmlPullParserUtil.isStartTag(xpp, "ProgramInformation")) { + programInformation = parseProgramInformation(xpp); + } else if (XmlPullParserUtil.isStartTag(xpp, "UTCTiming")) { + utcTiming = parseUtcTiming(xpp); + } else if (XmlPullParserUtil.isStartTag(xpp, "Location")) { + location = Uri.parse(xpp.nextText()); + } else if (XmlPullParserUtil.isStartTag(xpp, "ServiceDescription")) { + serviceDescription = parseServiceDescription(xpp); + } else if (XmlPullParserUtil.isStartTag(xpp, "Period") && !seenEarlyAccessPeriod) { + Pair periodWithDurationMs = + parsePeriod( + xpp, + baseUrl, + nextPeriodStartMs, + baseUrlAvailabilityTimeOffsetUs, + availabilityStartTime, + timeShiftBufferDepthMs); + CustomPeriod period = periodWithDurationMs.first; + if (period.startMs == C.TIME_UNSET) { + if (dynamic) { + // This is an early access period. Ignore it. All subsequent periods must also be + // early access. + seenEarlyAccessPeriod = true; + } else { + throw new ParserException("Unable to determine start of period " + periods.size()); + } + } else { + long periodDurationMs = periodWithDurationMs.second; + nextPeriodStartMs = periodDurationMs == C.TIME_UNSET ? C.TIME_UNSET + : (period.startMs + periodDurationMs); + periods.add(period); + } + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "MPD")); + + if (durationMs == C.TIME_UNSET) { + if (nextPeriodStartMs != C.TIME_UNSET) { + // If we know the end time of the final period, we can use it as the duration. + durationMs = nextPeriodStartMs; + } else if (!dynamic) { + throw new ParserException("Unable to determine duration of static manifest."); + } + } + + if (periods.isEmpty()) { + throw new ParserException("No periods found."); + } + + return buildMediaPresentationDescription( + availabilityStartTime, + durationMs, + minBufferTimeMs, + dynamic, + minUpdateTimeMs, + timeShiftBufferDepthMs, + suggestedPresentationDelayMs, + publishTimeMs, + programInformation, + utcTiming, + serviceDescription, + location, + periods); + } + + protected CustomDashManifest buildMediaPresentationDescription( + long availabilityStartTime, + long durationMs, + long minBufferTimeMs, + boolean dynamic, + long minUpdateTimeMs, + long timeShiftBufferDepthMs, + long suggestedPresentationDelayMs, + long publishTimeMs, + @Nullable ProgramInformation programInformation, + @Nullable UtcTimingElement utcTiming, + @Nullable ServiceDescriptionElement serviceDescription, + @Nullable Uri location, + List periods) { + return new CustomDashManifest( + availabilityStartTime, + durationMs, + minBufferTimeMs, + dynamic, + minUpdateTimeMs, + timeShiftBufferDepthMs, + suggestedPresentationDelayMs, + publishTimeMs, + programInformation, + utcTiming, + serviceDescription, + location, + periods); + } + + protected UtcTimingElement parseUtcTiming(XmlPullParser xpp) { + String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); + String value = xpp.getAttributeValue(null, "value"); + return buildUtcTimingElement(schemeIdUri, value); + } + + protected UtcTimingElement buildUtcTimingElement(String schemeIdUri, String value) { + return new UtcTimingElement(schemeIdUri, value); + } + + protected ServiceDescriptionElement parseServiceDescription(XmlPullParser xpp) + throws XmlPullParserException, IOException { + long targetOffsetMs = C.TIME_UNSET; + long minOffsetMs = C.TIME_UNSET; + long maxOffsetMs = C.TIME_UNSET; + float minPlaybackSpeed = C.RATE_UNSET; + float maxPlaybackSpeed = C.RATE_UNSET; + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "Latency")) { + targetOffsetMs = parseLong(xpp, "target", C.TIME_UNSET); + minOffsetMs = parseLong(xpp, "min", C.TIME_UNSET); + maxOffsetMs = parseLong(xpp, "max", C.TIME_UNSET); + } else if (XmlPullParserUtil.isStartTag(xpp, "PlaybackRate")) { + minPlaybackSpeed = parseFloat(xpp, "min", C.RATE_UNSET); + maxPlaybackSpeed = parseFloat(xpp, "max", C.RATE_UNSET); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "ServiceDescription")); + return new ServiceDescriptionElement( + targetOffsetMs, minOffsetMs, maxOffsetMs, minPlaybackSpeed, maxPlaybackSpeed); + } + + protected Pair parsePeriod( + XmlPullParser xpp, + String baseUrl, + long defaultStartMs, + long baseUrlAvailabilityTimeOffsetUs, + long availabilityStartTimeMs, + long timeShiftBufferDepthMs) + throws XmlPullParserException, IOException { + @Nullable String id = xpp.getAttributeValue(null, "id"); + long startMs = parseDuration(xpp, "start", defaultStartMs); + long periodStartUnixTimeMs = + availabilityStartTimeMs != C.TIME_UNSET ? availabilityStartTimeMs + startMs : C.TIME_UNSET; + long durationMs = parseDuration(xpp, "duration", C.TIME_UNSET); + @Nullable CustomSegmentBase segmentBase = null; + @Nullable Descriptor assetIdentifier = null; + List adaptationSets = new ArrayList<>(); + List eventStreams = new ArrayList<>(); + boolean seenFirstBaseUrl = false; + long segmentBaseAvailabilityTimeOffsetUs = C.TIME_UNSET; + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "BaseURL")) { + if (!seenFirstBaseUrl) { + baseUrlAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, baseUrlAvailabilityTimeOffsetUs); + baseUrl = parseBaseUrl(xpp, baseUrl); + seenFirstBaseUrl = true; + } + } else if (XmlPullParserUtil.isStartTag(xpp, "AdaptationSet")) { + adaptationSets.add( + parseAdaptationSet( + xpp, + baseUrl, + segmentBase, + durationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs, + periodStartUnixTimeMs, + timeShiftBufferDepthMs)); + } else if (XmlPullParserUtil.isStartTag(xpp, "EventStream")) { + eventStreams.add(parseEventStream(xpp)); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { + segmentBase = parseSegmentBase(xpp, /* parent= */ null); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { + segmentBaseAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, /* parentAvailabilityTimeOffsetUs= */ C.TIME_UNSET); + segmentBase = + parseSegmentList( + xpp, + /* parent= */ null, + periodStartUnixTimeMs, + durationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs, + timeShiftBufferDepthMs); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { + segmentBaseAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, /* parentAvailabilityTimeOffsetUs= */ C.TIME_UNSET); + segmentBase = + parseSegmentTemplate( + xpp, + /* parent= */ null, + new ArrayList<>(), + periodStartUnixTimeMs, + durationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs, + timeShiftBufferDepthMs); + } else if (XmlPullParserUtil.isStartTag(xpp, "AssetIdentifier")) { + assetIdentifier = parseDescriptor(xpp, "AssetIdentifier"); + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "Period")); + + return Pair.create( + buildPeriod(id, startMs, adaptationSets, eventStreams, assetIdentifier), durationMs); + } + + protected CustomPeriod buildPeriod( + @Nullable String id, + long startMs, + List adaptationSets, + List eventStreams, + @Nullable Descriptor assetIdentifier) { + return new CustomPeriod(id, startMs, adaptationSets, eventStreams, assetIdentifier); + } + + // AdaptationSet parsing. + + protected CustomAdaptationSet parseAdaptationSet( + XmlPullParser xpp, + String baseUrl, + @Nullable CustomSegmentBase segmentBase, + long periodDurationMs, + long baseUrlAvailabilityTimeOffsetUs, + long segmentBaseAvailabilityTimeOffsetUs, + long periodStartUnixTimeMs, + long timeShiftBufferDepthMs) + throws XmlPullParserException, IOException { + int id = parseInt(xpp, "id", AdaptationSet.ID_UNSET); + int contentType = parseContentType(xpp); + + String mimeType = xpp.getAttributeValue(null, "mimeType"); + + if (contentType == -1 && ("image/jpeg".equals(mimeType) || "image/png".equals(mimeType))) { + contentType = C.TRACK_TYPE_IMAGE; + } + String codecs = xpp.getAttributeValue(null, "codecs"); + int width = parseInt(xpp, "width", Format.NO_VALUE); + int height = parseInt(xpp, "height", Format.NO_VALUE); + float frameRate = parseFrameRate(xpp, Format.NO_VALUE); + int audioChannels = Format.NO_VALUE; + int audioSamplingRate = parseInt(xpp, "audioSamplingRate", Format.NO_VALUE); + String language = xpp.getAttributeValue(null, "lang"); + String label = xpp.getAttributeValue(null, "label"); + String drmSchemeType = null; + ArrayList drmSchemeDatas = new ArrayList<>(); + ArrayList inbandEventStreams = new ArrayList<>(); + ArrayList accessibilityDescriptors = new ArrayList<>(); + ArrayList roleDescriptors = new ArrayList<>(); + ArrayList essentialProperties = new ArrayList<>(); + ArrayList supplementalProperties = new ArrayList<>(); + List representationInfos = new ArrayList<>(); + + boolean seenFirstBaseUrl = false; + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "BaseURL")) { + if (!seenFirstBaseUrl) { + baseUrlAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, baseUrlAvailabilityTimeOffsetUs); + baseUrl = parseBaseUrl(xpp, baseUrl); + seenFirstBaseUrl = true; + } + } else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) { + Pair contentProtection = parseContentProtection(xpp); + if (contentProtection.first != null) { + drmSchemeType = contentProtection.first; + } + if (contentProtection.second != null) { + drmSchemeDatas.add(contentProtection.second); + } + } else if (XmlPullParserUtil.isStartTag(xpp, "ContentComponent")) { + language = checkLanguageConsistency(language, xpp.getAttributeValue(null, "lang")); + contentType = checkContentTypeConsistency(contentType, parseContentType(xpp)); + } else if (XmlPullParserUtil.isStartTag(xpp, "Role")) { + roleDescriptors.add(parseDescriptor(xpp, "Role")); + } else if (XmlPullParserUtil.isStartTag(xpp, "AudioChannelConfiguration")) { + audioChannels = parseAudioChannelConfiguration(xpp); + } else if (XmlPullParserUtil.isStartTag(xpp, "Accessibility")) { + accessibilityDescriptors.add(parseDescriptor(xpp, "Accessibility")); + } else if (XmlPullParserUtil.isStartTag(xpp, "EssentialProperty")) { + essentialProperties.add(parseDescriptor(xpp, "EssentialProperty")); + } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) { + supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty")); + } else if (XmlPullParserUtil.isStartTag(xpp, "Representation")) { + RepresentationInfo representationInfo = + parseRepresentation( + xpp, + baseUrl, + mimeType, + codecs, + width, + height, + frameRate, + audioChannels, + audioSamplingRate, + language, + roleDescriptors, + accessibilityDescriptors, + essentialProperties, + supplementalProperties, + segmentBase, + periodStartUnixTimeMs, + periodDurationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs, + timeShiftBufferDepthMs); + contentType = + checkContentTypeConsistency( + contentType, MimeTypes.getTrackType(representationInfo.format.sampleMimeType)); + representationInfos.add(representationInfo); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { + segmentBase = parseSegmentBase(xpp, (CustomSegmentBase.SingleSegmentBase) segmentBase); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { + segmentBaseAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, segmentBaseAvailabilityTimeOffsetUs); + segmentBase = + parseSegmentList( + xpp, + (SegmentList) segmentBase, + periodStartUnixTimeMs, + periodDurationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs, + timeShiftBufferDepthMs); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { + segmentBaseAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, segmentBaseAvailabilityTimeOffsetUs); + segmentBase = + parseSegmentTemplate( + xpp, + (CustomSegmentBase.SegmentTemplate) segmentBase, + supplementalProperties, + periodStartUnixTimeMs, + periodDurationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs, + timeShiftBufferDepthMs); + } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { + inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); + } else if (XmlPullParserUtil.isStartTag(xpp, "Label")) { + label = parseLabel(xpp); + } else if (XmlPullParserUtil.isStartTag(xpp)) { + parseAdaptationSetChild(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "AdaptationSet")); + + // Build the representations. + List representations = new ArrayList<>(representationInfos.size()); + for (int i = 0; i < representationInfos.size(); i++) { + representations.add( + buildRepresentation( + representationInfos.get(i), + label, + drmSchemeType, + drmSchemeDatas, + inbandEventStreams)); + } + + return buildAdaptationSet( + id, + contentType, + representations, + accessibilityDescriptors, + essentialProperties, + supplementalProperties); + } + + protected CustomAdaptationSet buildAdaptationSet( + int id, + int contentType, + List representations, + List accessibilityDescriptors, + List essentialProperties, + List supplementalProperties) { + return new CustomAdaptationSet( + id, + contentType, + representations, + accessibilityDescriptors, + essentialProperties, + supplementalProperties); + } + + protected int parseContentType(XmlPullParser xpp) { + String contentType = xpp.getAttributeValue(null, "contentType"); + return TextUtils.isEmpty(contentType) ? C.TRACK_TYPE_UNKNOWN + : MimeTypes.BASE_TYPE_AUDIO.equals(contentType) ? C.TRACK_TYPE_AUDIO + : MimeTypes.BASE_TYPE_VIDEO.equals(contentType) ? C.TRACK_TYPE_VIDEO + : MimeTypes.BASE_TYPE_TEXT.equals(contentType) ? C.TRACK_TYPE_TEXT + : BASE_TYPE_IMAGE.equals(contentType) ? C.TRACK_TYPE_IMAGE + : C.TRACK_TYPE_UNKNOWN; + } + + /** + * Parses a ContentProtection element. + * + * @param xpp The parser from which to read. + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + * @return The scheme type and/or {@link SchemeData} parsed from the ContentProtection element. + * Either or both may be null, depending on the ContentProtection element being parsed. + */ + protected Pair parseContentProtection( + XmlPullParser xpp) throws XmlPullParserException, IOException { + String schemeType = null; + String licenseServerUrl = null; + byte[] data = null; + UUID uuid = null; + + String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); + if (schemeIdUri != null) { + switch (Util.toLowerInvariant(schemeIdUri)) { + case "urn:mpeg:dash:mp4protection:2011": + schemeType = xpp.getAttributeValue(null, "value"); + String defaultKid = XmlPullParserUtil.getAttributeValueIgnorePrefix(xpp, "default_KID"); + if (!TextUtils.isEmpty(defaultKid) + && !"00000000-0000-0000-0000-000000000000".equals(defaultKid)) { + String[] defaultKidStrings = defaultKid.split("\\s+"); + UUID[] defaultKids = new UUID[defaultKidStrings.length]; + for (int i = 0; i < defaultKidStrings.length; i++) { + defaultKids[i] = UUID.fromString(defaultKidStrings[i]); + } + data = PsshAtomUtil.buildPsshAtom(C.COMMON_PSSH_UUID, defaultKids, null); + uuid = C.COMMON_PSSH_UUID; + } + break; + case "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95": + uuid = C.PLAYREADY_UUID; + break; + case "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed": + uuid = C.WIDEVINE_UUID; + break; + default: + break; + } + } + + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "ms:laurl")) { + licenseServerUrl = xpp.getAttributeValue(null, "licenseUrl"); + } else if (data == null + && XmlPullParserUtil.isStartTagIgnorePrefix(xpp, "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"); + data = null; + } + } else if (data == null + && C.PLAYREADY_UUID.equals(uuid) + && XmlPullParserUtil.isStartTag(xpp, "mspr:pro") + && xpp.next() == XmlPullParser.TEXT) { + // The mspr:pro element is defined in DASH Content Protection using Microsoft PlayReady. + data = + PsshAtomUtil.buildPsshAtom( + C.PLAYREADY_UUID, Base64.decode(xpp.getText(), Base64.DEFAULT)); + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection")); + SchemeData schemeData = + uuid != null ? new SchemeData(uuid, licenseServerUrl, MimeTypes.VIDEO_MP4, data) : null; + return Pair.create(schemeType, schemeData); + } + + /** + * Parses children of AdaptationSet elements not specifically parsed elsewhere. + * + * @param xpp The XmpPullParser from which the AdaptationSet child should be parsed. + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + */ + protected void parseAdaptationSetChild(XmlPullParser xpp) + throws XmlPullParserException, IOException { + maybeSkipTag(xpp); + } + + // Representation parsing. + + protected RepresentationInfo parseRepresentation( + XmlPullParser xpp, + String baseUrl, + @Nullable String adaptationSetMimeType, + @Nullable String adaptationSetCodecs, + int adaptationSetWidth, + int adaptationSetHeight, + float adaptationSetFrameRate, + int adaptationSetAudioChannels, + int adaptationSetAudioSamplingRate, + @Nullable String adaptationSetLanguage, + List adaptationSetRoleDescriptors, + List adaptationSetAccessibilityDescriptors, + List adaptationSetEssentialProperties, + List adaptationSetSupplementalProperties, + @Nullable CustomSegmentBase segmentBase, + long periodStartUnixTimeMs, + long periodDurationMs, + long baseUrlAvailabilityTimeOffsetUs, + long segmentBaseAvailabilityTimeOffsetUs, + long timeShiftBufferDepthMs) + throws XmlPullParserException, IOException { + String id = xpp.getAttributeValue(null, "id"); + int bandwidth = parseInt(xpp, "bandwidth", Format.NO_VALUE); + + String mimeType = parseString(xpp, "mimeType", adaptationSetMimeType); + + String codecs = parseString(xpp, "codecs", adaptationSetCodecs); + int width = parseInt(xpp, "width", adaptationSetWidth); + int height = parseInt(xpp, "height", adaptationSetHeight); + float frameRate = parseFrameRate(xpp, adaptationSetFrameRate); + int audioChannels = adaptationSetAudioChannels; + int audioSamplingRate = parseInt(xpp, "audioSamplingRate", adaptationSetAudioSamplingRate); + String drmSchemeType = null; + ArrayList drmSchemeDatas = new ArrayList<>(); + ArrayList inbandEventStreams = new ArrayList<>(); + ArrayList essentialProperties = new ArrayList<>(adaptationSetEssentialProperties); + ArrayList supplementalProperties = + new ArrayList<>(adaptationSetSupplementalProperties); + + boolean seenFirstBaseUrl = false; + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "BaseURL")) { + if (!seenFirstBaseUrl) { + baseUrlAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, baseUrlAvailabilityTimeOffsetUs); + baseUrl = parseBaseUrl(xpp, baseUrl); + seenFirstBaseUrl = true; + } + } else if (XmlPullParserUtil.isStartTag(xpp, "AudioChannelConfiguration")) { + audioChannels = parseAudioChannelConfiguration(xpp); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { + segmentBase = parseSegmentBase(xpp, (SingleSegmentBase) segmentBase); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { + segmentBaseAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, segmentBaseAvailabilityTimeOffsetUs); + segmentBase = + parseSegmentList( + xpp, + (SegmentList) segmentBase, + periodStartUnixTimeMs, + periodDurationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs, + timeShiftBufferDepthMs); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { + segmentBaseAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, segmentBaseAvailabilityTimeOffsetUs); + segmentBase = + parseSegmentTemplate( + xpp, + (SegmentTemplate) segmentBase, + adaptationSetSupplementalProperties, + periodStartUnixTimeMs, + periodDurationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs, + timeShiftBufferDepthMs); + } else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) { + Pair contentProtection = parseContentProtection(xpp); + if (contentProtection.first != null) { + drmSchemeType = contentProtection.first; + } + if (contentProtection.second != null) { + drmSchemeDatas.add(contentProtection.second); + } + } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { + inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); + } else if (XmlPullParserUtil.isStartTag(xpp, "EssentialProperty")) { + essentialProperties.add(parseDescriptor(xpp, "EssentialProperty")); + } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) { + supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty")); + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "Representation")); + + CustomFormat format = + buildFormat( + id, + mimeType, + width, + height, + frameRate, + audioChannels, + audioSamplingRate, + bandwidth, + adaptationSetLanguage, + adaptationSetRoleDescriptors, + adaptationSetAccessibilityDescriptors, + codecs, + essentialProperties, + supplementalProperties, + segmentBase, + baseUrl); + + segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase(); + + return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeType, drmSchemeDatas, + inbandEventStreams, Representation.REVISION_ID_DEFAULT); } + + protected CustomFormat buildFormat( + @Nullable String id, + @Nullable String containerMimeType, + int width, + int height, + float frameRate, + int audioChannels, + int audioSamplingRate, + int bitrate, + @Nullable String language, + List roleDescriptors, + List accessibilityDescriptors, + @Nullable String codecs, + List essentialProperties, + List supplementalProperties, + CustomSegmentBase segmentBase, + String baseURL) { + @Nullable String sampleMimeType = getSampleMimeType(containerMimeType, codecs); + if (MimeTypes.AUDIO_E_AC3.equals(sampleMimeType)) { + sampleMimeType = parseEac3SupplementalProperties(supplementalProperties); + } + CustomFormat.FormatThumbnailInfo formatThumbnailInfo = null; + if (isImage(containerMimeType)) { + formatThumbnailInfo = buildFormatThumbnailInfo(baseURL, id, bitrate , segmentBase, essentialProperties); + } + @C.SelectionFlags int selectionFlags = parseSelectionFlagsFromRoleDescriptors(roleDescriptors); + @C.RoleFlags int roleFlags = parseRoleFlagsFromRoleDescriptors(roleDescriptors); + roleFlags |= parseRoleFlagsFromAccessibilityDescriptors(accessibilityDescriptors); + roleFlags |= parseRoleFlagsFromProperties(essentialProperties); + roleFlags |= parseRoleFlagsFromProperties(supplementalProperties); + + CustomFormat.Builder formatBuilder = + new CustomFormat.Builder() + .setId(id) + .setContainerMimeType(containerMimeType) + .setSampleMimeType(sampleMimeType) + .setCodecs(codecs) + .setPeakBitrate(bitrate) + .setSelectionFlags(selectionFlags) + .setRoleFlags(roleFlags) + .setLanguage(language); + + if (isImage(containerMimeType)) { + formatBuilder.setFormatThumbnailInfo(formatThumbnailInfo); + formatBuilder.setWidth(width).setHeight(height); + } + + if (MimeTypes.isVideo(sampleMimeType)) { + formatBuilder.setWidth(width).setHeight(height).setFrameRate(frameRate); + } else if (MimeTypes.isAudio(sampleMimeType)) { + formatBuilder.setChannelCount(audioChannels).setSampleRate(audioSamplingRate); + } else if (MimeTypes.isText(sampleMimeType)) { + int accessibilityChannel = Format.NO_VALUE; + if (MimeTypes.APPLICATION_CEA608.equals(sampleMimeType)) { + accessibilityChannel = parseCea608AccessibilityChannel(accessibilityDescriptors); + } else if (MimeTypes.APPLICATION_CEA708.equals(sampleMimeType)) { + accessibilityChannel = parseCea708AccessibilityChannel(accessibilityDescriptors); + } + formatBuilder.setAccessibilityChannel(accessibilityChannel); + } + + return formatBuilder.build(); + } + + /** Returns whether the given string is a image MIME type. */ + public static boolean isImage(@Nullable String mimeType) { + return BASE_TYPE_IMAGE.equals(getTopLevelType(mimeType)); + } + + private static String getTopLevelType(@Nullable String mimeType) { + if (mimeType == null) { + return null; + } + int indexOfSlash = mimeType.indexOf('/'); + if (indexOfSlash == -1) { + return null; + } + return mimeType.substring(0, indexOfSlash); + } + + protected CustomRepresentation buildRepresentation( + RepresentationInfo representationInfo, + @Nullable String label, + @Nullable String extraDrmSchemeType, + ArrayList extraDrmSchemeDatas, + ArrayList extraInbandEventStreams) { + CustomFormat.Builder formatBuilder = representationInfo.format.buildUpon(); + if (label != null) { + formatBuilder.setLabel(label); + } + if (isImage(representationInfo.format.containerMimeType)) { + if (representationInfo.format.formatThumbnailInfo == null) { + formatBuilder.setFormatThumbnailInfo(buildFormatThumbnailInfo(representationInfo.baseUrl, representationInfo.format.id, representationInfo.format.bitrate, representationInfo.segmentBase, null)); + } else { + formatBuilder.setFormatThumbnailInfo(representationInfo.format.formatThumbnailInfo); + } + } + + @Nullable String drmSchemeType = representationInfo.drmSchemeType; + if (drmSchemeType == null) { + drmSchemeType = extraDrmSchemeType; + } + ArrayList drmSchemeDatas = representationInfo.drmSchemeDatas; + drmSchemeDatas.addAll(extraDrmSchemeDatas); + if (!drmSchemeDatas.isEmpty()) { + filterRedundantIncompleteSchemeDatas(drmSchemeDatas); + formatBuilder.setDrmInitData(new DrmInitData(drmSchemeType, drmSchemeDatas)); + } + ArrayList inbandEventStreams = representationInfo.inbandEventStreams; + inbandEventStreams.addAll(extraInbandEventStreams); + return CustomRepresentation.newInstance( + representationInfo.revisionId, + formatBuilder.build(), + representationInfo.baseUrl, + representationInfo.segmentBase, + inbandEventStreams); + } + + private CustomFormat.FormatThumbnailInfo buildFormatThumbnailInfo(String baseURL, String id, int bitrate, CustomSegmentBase segmentBase, List essentialProperties) { + CustomFormat.FormatThumbnailInfo.Builder thumbnailInfoBuilder = new CustomFormat.FormatThumbnailInfo.Builder(); + String structure = ""; + int tilesHorizontal = 1; + int tilesVertical = 1; + + if (essentialProperties != null && essentialProperties.size() == 1) { + structure = essentialProperties.get(0).value; + if (!TextUtils.isEmpty(structure) && structure.contains("x")) { + String[] structureContent = structure.split("x"); + if (TextUtils.isDigitsOnly(structureContent[0]) && TextUtils.isDigitsOnly(structureContent[1])) { + tilesHorizontal = Integer.parseInt(structureContent[0]); + tilesVertical = Integer.parseInt(structureContent[1]); + } + } + } + long presentationTimeOffset = ((SegmentTemplate)segmentBase).getPresentationTimeOffset(); + long timeScale = ((SegmentTemplate)segmentBase).getTimescale(); + long startNumber = ((SegmentTemplate)segmentBase).getStartNumber(); + long endNumber = ((SegmentTemplate)segmentBase).getEndNumber(); + UrlTemplate urlTemplate = ((SegmentTemplate)segmentBase).getInitializationTemplate(); + if (urlTemplate == null) { + urlTemplate = ((SegmentTemplate)segmentBase).mediaTemplate; + } + String imageTemplateUrl = ""; + if (urlTemplate != null) { + imageTemplateUrl = baseURL + urlTemplate.buildUri(id, 900000009, bitrate, 800000008); + imageTemplateUrl = imageTemplateUrl.replace("900000009", "$Number$").replace("800000008", "$Time$"); + + } + thumbnailInfoBuilder.setImageTemplateUrl(imageTemplateUrl); + thumbnailInfoBuilder.setSegmentDuration(((SegmentTemplate)segmentBase).duration); + thumbnailInfoBuilder.setStructure(structure); + thumbnailInfoBuilder.setTilesHorizontal(tilesHorizontal); + thumbnailInfoBuilder.setTilesVertical(tilesVertical); + thumbnailInfoBuilder.setPresentationTimeOffset(presentationTimeOffset); + thumbnailInfoBuilder.setTimeScale(timeScale); + thumbnailInfoBuilder.setStartNumber(startNumber); + thumbnailInfoBuilder.setEndNumber(endNumber); + return thumbnailInfoBuilder.build(); + } + + // SegmentBase, SegmentList and SegmentTemplate parsing. + + protected SingleSegmentBase parseSegmentBase( + XmlPullParser xpp, @Nullable CustomSegmentBase.SingleSegmentBase parent) + throws XmlPullParserException, IOException { + + long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); + long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", + parent != null ? parent.presentationTimeOffset : 0); + + long indexStart = parent != null ? parent.indexStart : 0; + long indexLength = parent != null ? parent.indexLength : 0; + String indexRangeText = xpp.getAttributeValue(null, "indexRange"); + if (indexRangeText != null) { + String[] indexRange = indexRangeText.split("-"); + indexStart = Long.parseLong(indexRange[0]); + indexLength = Long.parseLong(indexRange[1]) - indexStart + 1; + } + + @Nullable RangedUri initialization = parent != null ? parent.initialization : null; + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "Initialization")) { + initialization = parseInitialization(xpp); + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "SegmentBase")); + + return buildSingleSegmentBase(initialization, timescale, presentationTimeOffset, indexStart, + indexLength); + } + + protected SingleSegmentBase buildSingleSegmentBase(RangedUri initialization, long timescale, + long presentationTimeOffset, long indexStart, long indexLength) { + return new SingleSegmentBase(initialization, timescale, presentationTimeOffset, indexStart, + indexLength); + } + + protected SegmentList parseSegmentList( + XmlPullParser xpp, + @Nullable SegmentList parent, + long periodStartUnixTimeMs, + long periodDurationMs, + long baseUrlAvailabilityTimeOffsetUs, + long segmentBaseAvailabilityTimeOffsetUs, + long timeShiftBufferDepthMs) + throws XmlPullParserException, IOException { + + long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); + long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", + parent != null ? parent.presentationTimeOffset : 0); + long duration = parseLong(xpp, "duration", parent != null ? parent.duration : C.TIME_UNSET); + long startNumber = parseLong(xpp, "startNumber", parent != null ? parent.startNumber : 1); + long availabilityTimeOffsetUs = + getFinalAvailabilityTimeOffset( + baseUrlAvailabilityTimeOffsetUs, segmentBaseAvailabilityTimeOffsetUs); + + RangedUri initialization = null; + List timeline = null; + List segments = null; + + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "Initialization")) { + initialization = parseInitialization(xpp); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTimeline")) { + timeline = parseSegmentTimeline(xpp, timescale, periodDurationMs); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentURL")) { + if (segments == null) { + segments = new ArrayList<>(); + } + segments.add(parseSegmentUrl(xpp)); + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "SegmentList")); + + if (parent != null) { + initialization = initialization != null ? initialization : parent.initialization; + timeline = timeline != null ? timeline : parent.segmentTimeline; + segments = segments != null ? segments : parent.mediaSegments; + } + + return buildSegmentList( + initialization, + timescale, + presentationTimeOffset, + startNumber, + duration, + timeline, + availabilityTimeOffsetUs, + segments, + timeShiftBufferDepthMs, + periodStartUnixTimeMs); + } + + protected SegmentList buildSegmentList( + RangedUri initialization, + long timescale, + long presentationTimeOffset, + long startNumber, + long duration, + @Nullable List timeline, + long availabilityTimeOffsetUs, + @Nullable List segments, + long timeShiftBufferDepthMs, + long periodStartUnixTimeMs) { + return new SegmentList( + initialization, + timescale, + presentationTimeOffset, + startNumber, + duration, + timeline, + availabilityTimeOffsetUs, + segments, + C.msToUs(timeShiftBufferDepthMs), + C.msToUs(periodStartUnixTimeMs)); + } + + protected SegmentTemplate parseSegmentTemplate( + XmlPullParser xpp, + @Nullable SegmentTemplate parent, + List adaptationSetSupplementalProperties, + long periodStartUnixTimeMs, + long periodDurationMs, + long baseUrlAvailabilityTimeOffsetUs, + long segmentBaseAvailabilityTimeOffsetUs, + long timeShiftBufferDepthMs) + throws XmlPullParserException, IOException { + long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); + long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", + parent != null ? parent.presentationTimeOffset : 0); + long duration = parseLong(xpp, "duration", parent != null ? parent.duration : C.TIME_UNSET); + long startNumber = parseLong(xpp, "startNumber", parent != null ? parent.startNumber : 1); + long endNumber = + parseLastSegmentNumberSupplementalProperty(adaptationSetSupplementalProperties); + long availabilityTimeOffsetUs = + getFinalAvailabilityTimeOffset( + baseUrlAvailabilityTimeOffsetUs, segmentBaseAvailabilityTimeOffsetUs); + + UrlTemplate mediaTemplate = parseUrlTemplate(xpp, "media", + parent != null ? parent.mediaTemplate : null); + UrlTemplate initializationTemplate = parseUrlTemplate(xpp, "initialization", + parent != null ? parent.initializationTemplate : null); + + RangedUri initialization = null; + List timeline = null; + + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "Initialization")) { + initialization = parseInitialization(xpp); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTimeline")) { + timeline = parseSegmentTimeline(xpp, timescale, periodDurationMs); + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "SegmentTemplate")); + + if (parent != null) { + initialization = initialization != null ? initialization : parent.initialization; + timeline = timeline != null ? timeline : parent.segmentTimeline; + } + + return buildSegmentTemplate( + initialization, + timescale, + presentationTimeOffset, + startNumber, + endNumber, + duration, + timeline, + availabilityTimeOffsetUs, + initializationTemplate, + mediaTemplate, + timeShiftBufferDepthMs, + periodStartUnixTimeMs); + } + + protected SegmentTemplate buildSegmentTemplate( + RangedUri initialization, + long timescale, + long presentationTimeOffset, + long startNumber, + long endNumber, + long duration, + List timeline, + long availabilityTimeOffsetUs, + @Nullable UrlTemplate initializationTemplate, + @Nullable UrlTemplate mediaTemplate, + long timeShiftBufferDepthMs, + long periodStartUnixTimeMs) { + return new SegmentTemplate( + initialization, + timescale, + presentationTimeOffset, + startNumber, + endNumber, + duration, + timeline, + availabilityTimeOffsetUs, + initializationTemplate, + mediaTemplate, + C.msToUs(timeShiftBufferDepthMs), + C.msToUs(periodStartUnixTimeMs)); + } + + /** + * Parses a single EventStream node in the manifest. + * + * @param xpp The current xml parser. + * @return The {@link EventStream} parsed from this EventStream node. + * @throws XmlPullParserException If there is any error parsing this node. + * @throws IOException If there is any error reading from the underlying input stream. + */ + protected EventStream parseEventStream(XmlPullParser xpp) + throws XmlPullParserException, IOException { + String schemeIdUri = parseString(xpp, "schemeIdUri", ""); + String value = parseString(xpp, "value", ""); + long timescale = parseLong(xpp, "timescale", 1); + List> eventMessages = new ArrayList<>(); + ByteArrayOutputStream scratchOutputStream = new ByteArrayOutputStream(512); + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "Event")) { + Pair event = + parseEvent(xpp, schemeIdUri, value, timescale, scratchOutputStream); + eventMessages.add(event); + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "EventStream")); + + long[] presentationTimesUs = new long[eventMessages.size()]; + EventMessage[] events = new EventMessage[eventMessages.size()]; + for (int i = 0; i < eventMessages.size(); i++) { + Pair event = eventMessages.get(i); + presentationTimesUs[i] = event.first; + events[i] = event.second; + } + return buildEventStream(schemeIdUri, value, timescale, presentationTimesUs, events); + } + + protected EventStream buildEventStream(String schemeIdUri, String value, long timescale, + long[] presentationTimesUs, EventMessage[] events) { + return new EventStream(schemeIdUri, value, timescale, presentationTimesUs, events); + } + + /** + * Parses a single Event node in the manifest. + * + * @param xpp The current xml parser. + * @param schemeIdUri The schemeIdUri of the parent EventStream. + * @param value The schemeIdUri of the parent EventStream. + * @param timescale The timescale of the parent EventStream. + * @param scratchOutputStream A {@link ByteArrayOutputStream} that is used when parsing event + * objects. + * @return A pair containing the node's presentation timestamp in microseconds and the parsed + * {@link EventMessage}. + * @throws XmlPullParserException If there is any error parsing this node. + * @throws IOException If there is any error reading from the underlying input stream. + */ + protected Pair parseEvent( + XmlPullParser xpp, + String schemeIdUri, + String value, + long timescale, + ByteArrayOutputStream scratchOutputStream) + throws IOException, XmlPullParserException { + long id = parseLong(xpp, "id", 0); + long duration = parseLong(xpp, "duration", C.TIME_UNSET); + long presentationTime = parseLong(xpp, "presentationTime", 0); + long durationMs = Util.scaleLargeTimestamp(duration, C.MILLIS_PER_SECOND, timescale); + long presentationTimesUs = Util.scaleLargeTimestamp(presentationTime, C.MICROS_PER_SECOND, + timescale); + String messageData = parseString(xpp, "messageData", null); + byte[] eventObject = parseEventObject(xpp, scratchOutputStream); + return Pair.create( + presentationTimesUs, + buildEvent( + schemeIdUri, + value, + id, + durationMs, + messageData == null ? eventObject : Util.getUtf8Bytes(messageData))); + } + + /** + * Parses an event object. + * + * @param xpp The current xml parser. + * @param scratchOutputStream A {@link ByteArrayOutputStream} that's used when parsing the object. + * @return The serialized byte array. + * @throws XmlPullParserException If there is any error parsing this node. + * @throws IOException If there is any error reading from the underlying input stream. + */ + protected byte[] parseEventObject(XmlPullParser xpp, ByteArrayOutputStream scratchOutputStream) + throws XmlPullParserException, IOException { + scratchOutputStream.reset(); + XmlSerializer xmlSerializer = Xml.newSerializer(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + xmlSerializer.setOutput(scratchOutputStream, StandardCharsets.UTF_8.name()); + } + // Start reading everything between and , and serialize them into an Xml + // byte array. + xpp.nextToken(); + while (!XmlPullParserUtil.isEndTag(xpp, "Event")) { + switch (xpp.getEventType()) { + case (XmlPullParser.START_DOCUMENT): + xmlSerializer.startDocument(null, false); + break; + case (XmlPullParser.END_DOCUMENT): + xmlSerializer.endDocument(); + break; + case (XmlPullParser.START_TAG): + xmlSerializer.startTag(xpp.getNamespace(), xpp.getName()); + for (int i = 0; i < xpp.getAttributeCount(); i++) { + xmlSerializer.attribute(xpp.getAttributeNamespace(i), xpp.getAttributeName(i), + xpp.getAttributeValue(i)); + } + break; + case (XmlPullParser.END_TAG): + xmlSerializer.endTag(xpp.getNamespace(), xpp.getName()); + break; + case (XmlPullParser.TEXT): + xmlSerializer.text(xpp.getText()); + break; + case (XmlPullParser.CDSECT): + xmlSerializer.cdsect(xpp.getText()); + break; + case (XmlPullParser.ENTITY_REF): + xmlSerializer.entityRef(xpp.getText()); + break; + case (XmlPullParser.IGNORABLE_WHITESPACE): + xmlSerializer.ignorableWhitespace(xpp.getText()); + break; + case (XmlPullParser.PROCESSING_INSTRUCTION): + xmlSerializer.processingInstruction(xpp.getText()); + break; + case (XmlPullParser.COMMENT): + xmlSerializer.comment(xpp.getText()); + break; + case (XmlPullParser.DOCDECL): + xmlSerializer.docdecl(xpp.getText()); + break; + default: // fall out + } + xpp.nextToken(); + } + xmlSerializer.flush(); + return scratchOutputStream.toByteArray(); + } + + protected EventMessage buildEvent( + String schemeIdUri, String value, long id, long durationMs, byte[] messageData) { + return new EventMessage(schemeIdUri, value, durationMs, id, messageData); + } + + protected List parseSegmentTimeline( + XmlPullParser xpp, long timescale, long periodDurationMs) + throws XmlPullParserException, IOException { + List segmentTimeline = new ArrayList<>(); + long startTime = 0; + long elementDuration = C.TIME_UNSET; + int elementRepeatCount = 0; + boolean havePreviousTimelineElement = false; + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "S")) { + long newStartTime = parseLong(xpp, "t", C.TIME_UNSET); + if (havePreviousTimelineElement) { + startTime = + addSegmentTimelineElementsToList( + segmentTimeline, + startTime, + elementDuration, + elementRepeatCount, + /* endTime= */ newStartTime); + } + if (newStartTime != C.TIME_UNSET) { + startTime = newStartTime; + } + elementDuration = parseLong(xpp, "d", C.TIME_UNSET); + elementRepeatCount = parseInt(xpp, "r", 0); + havePreviousTimelineElement = true; + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "SegmentTimeline")); + if (havePreviousTimelineElement) { + long periodDuration = Util.scaleLargeTimestamp(periodDurationMs, timescale, 1000); + addSegmentTimelineElementsToList( + segmentTimeline, + startTime, + elementDuration, + elementRepeatCount, + /* endTime= */ periodDuration); + } + return segmentTimeline; + } + + /** + * Adds timeline elements for one S tag to the segment timeline. + * + * @param startTime Start time of the first timeline element. + * @param elementDuration Duration of one timeline element. + * @param elementRepeatCount Number of timeline elements minus one. May be negative to indicate + * that the count is determined by the total duration and the element duration. + * @param endTime End time of the last timeline element for this S tag, or {@link C#TIME_UNSET} if + * unknown. Only needed if {@code repeatCount} is negative. + * @return Calculated next start time. + */ + private long addSegmentTimelineElementsToList( + List segmentTimeline, + long startTime, + long elementDuration, + int elementRepeatCount, + long endTime) { + int count = + elementRepeatCount >= 0 + ? 1 + elementRepeatCount + : (int) Util.ceilDivide(endTime - startTime, elementDuration); + for (int i = 0; i < count; i++) { + segmentTimeline.add(buildSegmentTimelineElement(startTime, elementDuration)); + startTime += elementDuration; + } + return startTime; + } + + protected SegmentTimelineElement buildSegmentTimelineElement(long startTime, long duration) { + return new SegmentTimelineElement(startTime, duration); + } + + @Nullable + protected UrlTemplate parseUrlTemplate( + XmlPullParser xpp, String name, @Nullable UrlTemplate defaultValue) { + String valueString = xpp.getAttributeValue(null, name); + if (valueString != null) { + return UrlTemplate.compile(valueString); + } + return defaultValue; + } + + protected RangedUri parseInitialization(XmlPullParser xpp) { + return parseRangedUrl(xpp, "sourceURL", "range"); + } + + protected RangedUri parseSegmentUrl(XmlPullParser xpp) { + return parseRangedUrl(xpp, "media", "mediaRange"); + } + + protected RangedUri parseRangedUrl(XmlPullParser xpp, String urlAttribute, + String rangeAttribute) { + String urlText = xpp.getAttributeValue(null, urlAttribute); + long rangeStart = 0; + long rangeLength = C.LENGTH_UNSET; + String rangeText = xpp.getAttributeValue(null, rangeAttribute); + if (rangeText != null) { + String[] rangeTextArray = rangeText.split("-"); + rangeStart = Long.parseLong(rangeTextArray[0]); + if (rangeTextArray.length == 2) { + rangeLength = Long.parseLong(rangeTextArray[1]) - rangeStart + 1; + } + } + return buildRangedUri(urlText, rangeStart, rangeLength); + } + + protected RangedUri buildRangedUri(String urlText, long rangeStart, long rangeLength) { + return new RangedUri(urlText, rangeStart, rangeLength); + } + + protected ProgramInformation parseProgramInformation(XmlPullParser xpp) + throws IOException, XmlPullParserException { + String title = null; + String source = null; + String copyright = null; + String moreInformationURL = parseString(xpp, "moreInformationURL", null); + String lang = parseString(xpp, "lang", null); + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "Title")) { + title = xpp.nextText(); + } else if (XmlPullParserUtil.isStartTag(xpp, "Source")) { + source = xpp.nextText(); + } else if (XmlPullParserUtil.isStartTag(xpp, "Copyright")) { + copyright = xpp.nextText(); + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "ProgramInformation")); + return new ProgramInformation(title, source, copyright, moreInformationURL, lang); + } + + /** + * Parses a Label element. + * + * @param xpp The parser from which to read. + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + * @return The parsed label. + */ + protected String parseLabel(XmlPullParser xpp) throws XmlPullParserException, IOException { + return parseText(xpp, "Label"); + } + + /** + * Parses a BaseURL element. + * + * @param xpp The parser from which to read. + * @param parentBaseUrl A base URL for resolving the parsed URL. + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + * @return The parsed and resolved URL. + */ + protected String parseBaseUrl(XmlPullParser xpp, String parentBaseUrl) + throws XmlPullParserException, IOException { + return UriUtil.resolve(parentBaseUrl, parseText(xpp, "BaseURL")); + } + + /** + * Parses the availabilityTimeOffset value and returns the parsed value or the parent value if it + * doesn't exist. + * + * @param xpp The parser from which to read. + * @param parentAvailabilityTimeOffsetUs The availability time offset of a parent element in + * microseconds. + * @return The parsed availabilityTimeOffset in microseconds. + */ + protected long parseAvailabilityTimeOffsetUs( + XmlPullParser xpp, long parentAvailabilityTimeOffsetUs) { + String value = xpp.getAttributeValue(/* namespace= */ null, "availabilityTimeOffset"); + if (value == null) { + return parentAvailabilityTimeOffsetUs; + } + if ("INF".equals(value)) { + return Long.MAX_VALUE; + } + return (long) (Float.parseFloat(value) * C.MICROS_PER_SECOND); + } + + // AudioChannelConfiguration parsing. + + protected int parseAudioChannelConfiguration(XmlPullParser xpp) + throws XmlPullParserException, IOException { + String schemeIdUri = parseString(xpp, "schemeIdUri", null); + int audioChannels; + switch (schemeIdUri) { + case "urn:mpeg:dash:23003:3:audio_channel_configuration:2011": + audioChannels = parseInt(xpp, "value", Format.NO_VALUE); + break; + case "urn:mpeg:mpegB:cicp:ChannelConfiguration": + audioChannels = parseMpegChannelConfiguration(xpp); + break; + case "tag:dolby.com,2014:dash:audio_channel_configuration:2011": + case "urn:dolby:dash:audio_channel_configuration:2011": + audioChannels = parseDolbyChannelConfiguration(xpp); + break; + default: + audioChannels = Format.NO_VALUE; + break; + } + do { + xpp.next(); + } while (!XmlPullParserUtil.isEndTag(xpp, "AudioChannelConfiguration")); + return audioChannels; + } + + // Selection flag parsing. + + protected int parseSelectionFlagsFromRoleDescriptors(List roleDescriptors) { + for (int i = 0; i < roleDescriptors.size(); i++) { + Descriptor descriptor = roleDescriptors.get(i); + if ("urn:mpeg:dash:role:2011".equalsIgnoreCase(descriptor.schemeIdUri) + && "main".equals(descriptor.value)) { + return C.SELECTION_FLAG_DEFAULT; + } + } + return 0; + } + + // Role and Accessibility parsing. + + @C.RoleFlags + protected int parseRoleFlagsFromRoleDescriptors(List roleDescriptors) { + @C.RoleFlags int result = 0; + for (int i = 0; i < roleDescriptors.size(); i++) { + Descriptor descriptor = roleDescriptors.get(i); + if ("urn:mpeg:dash:role:2011".equalsIgnoreCase(descriptor.schemeIdUri)) { + result |= parseDashRoleSchemeValue(descriptor.value); + } + } + return result; + } + + @C.RoleFlags + protected int parseRoleFlagsFromAccessibilityDescriptors( + List accessibilityDescriptors) { + @C.RoleFlags int result = 0; + for (int i = 0; i < accessibilityDescriptors.size(); i++) { + Descriptor descriptor = accessibilityDescriptors.get(i); + if ("urn:mpeg:dash:role:2011".equalsIgnoreCase(descriptor.schemeIdUri)) { + result |= parseDashRoleSchemeValue(descriptor.value); + } else if ("urn:tva:metadata:cs:AudioPurposeCS:2007" + .equalsIgnoreCase(descriptor.schemeIdUri)) { + result |= parseTvaAudioPurposeCsValue(descriptor.value); + } + } + return result; + } + + @C.RoleFlags + protected int parseRoleFlagsFromProperties(List accessibilityDescriptors) { + @C.RoleFlags int result = 0; + for (int i = 0; i < accessibilityDescriptors.size(); i++) { + Descriptor descriptor = accessibilityDescriptors.get(i); + if ("http://dashif.org/guidelines/trickmode".equalsIgnoreCase(descriptor.schemeIdUri)) { + result |= C.ROLE_FLAG_TRICK_PLAY; + } + } + return result; + } + + @C.RoleFlags + protected int parseDashRoleSchemeValue(@Nullable String value) { + if (value == null) { + return 0; + } + switch (value) { + case "main": + return C.ROLE_FLAG_MAIN; + case "alternate": + return C.ROLE_FLAG_ALTERNATE; + case "supplementary": + return C.ROLE_FLAG_SUPPLEMENTARY; + case "commentary": + return C.ROLE_FLAG_COMMENTARY; + case "dub": + return C.ROLE_FLAG_DUB; + case "emergency": + return C.ROLE_FLAG_EMERGENCY; + case "caption": + return C.ROLE_FLAG_CAPTION; + case "subtitle": + return C.ROLE_FLAG_SUBTITLE; + case "sign": + return C.ROLE_FLAG_SIGN; + case "description": + return C.ROLE_FLAG_DESCRIBES_VIDEO; + case "enhanced-audio-intelligibility": + return C.ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY; + default: + return 0; + } + } + + @C.RoleFlags + protected int parseTvaAudioPurposeCsValue(@Nullable String value) { + if (value == null) { + return 0; + } + switch (value) { + case "1": // Audio description for the visually impaired. + return C.ROLE_FLAG_DESCRIBES_VIDEO; + case "2": // Audio description for the hard of hearing. + return C.ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY; + case "3": // Supplemental commentary. + return C.ROLE_FLAG_SUPPLEMENTARY; + case "4": // Director's commentary. + return C.ROLE_FLAG_COMMENTARY; + case "6": // Main programme audio. + return C.ROLE_FLAG_MAIN; + default: + return 0; + } + } + + // Utility methods. + + /** + * If the provided {@link XmlPullParser} is currently positioned at the start of a tag, skips + * forward to the end of that tag. + * + * @param xpp The {@link XmlPullParser}. + * @throws XmlPullParserException If an error occurs parsing the stream. + * @throws IOException If an error occurs reading the stream. + */ + public static void maybeSkipTag(XmlPullParser xpp) throws IOException, XmlPullParserException { + if (!XmlPullParserUtil.isStartTag(xpp)) { + return; + } + int depth = 1; + while (depth != 0) { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp)) { + depth++; + } else if (XmlPullParserUtil.isEndTag(xpp)) { + depth--; + } + } + } + + /** + * Removes unnecessary {@link SchemeData}s with null {@link SchemeData#data}. + */ + private static void filterRedundantIncompleteSchemeDatas(ArrayList schemeDatas) { + for (int i = schemeDatas.size() - 1; i >= 0; i--) { + SchemeData schemeData = schemeDatas.get(i); + if (!schemeData.hasData()) { + for (int j = 0; j < schemeDatas.size(); j++) { + if (schemeDatas.get(j).canReplace(schemeData)) { + // schemeData is incomplete, but there is another matching SchemeData which does contain + // data, so we remove the incomplete one. + schemeDatas.remove(i); + break; + } + } + } + } + } + + /** + * Derives a sample mimeType from a container mimeType and codecs attribute. + * + * @param containerMimeType The mimeType of the container. + * @param codecs The codecs attribute. + * @return The derived sample mimeType, or null if it could not be derived. + */ + @Nullable + private static String getSampleMimeType( + @Nullable String containerMimeType, @Nullable String codecs) { + if (MimeTypes.isAudio(containerMimeType)) { + return MimeTypes.getAudioMediaMimeType(codecs); + } else if (MimeTypes.isVideo(containerMimeType)) { + return MimeTypes.getVideoMediaMimeType(codecs); + } else if (MimeTypes.isText(containerMimeType)) { + if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { + // RawCC is special because it's a text specific container format. + return MimeTypes.getTextMediaMimeType(codecs); + } + // All other text types are raw formats. + return containerMimeType; + } else if (MimeTypes.APPLICATION_MP4.equals(containerMimeType)) { + @Nullable String mimeType = MimeTypes.getMediaMimeType(codecs); + return MimeTypes.TEXT_VTT.equals(mimeType) ? MimeTypes.APPLICATION_MP4VTT : mimeType; + } + return null; + } + + /** + * Checks two languages for consistency, returning the consistent language, or throwing an {@link + * IllegalStateException} if the languages are inconsistent. + * + *

Two languages are consistent if they are equal, or if one is null. + * + * @param firstLanguage The first language. + * @param secondLanguage The second language. + * @return The consistent language. + */ + @Nullable + private static String checkLanguageConsistency( + @Nullable String firstLanguage, @Nullable String secondLanguage) { + if (firstLanguage == null) { + return secondLanguage; + } else if (secondLanguage == null) { + return firstLanguage; + } else { + Assertions.checkState(firstLanguage.equals(secondLanguage)); + return firstLanguage; + } + } + + /** + * Checks two adaptation set content types for consistency, returning the consistent type, or + * throwing an {@link IllegalStateException} if the types are inconsistent. + *

+ * Two types are consistent if they are equal, or if one is {@link C#TRACK_TYPE_UNKNOWN}. + * Where one of the types is {@link C#TRACK_TYPE_UNKNOWN}, the other is returned. + * + * @param firstType The first type. + * @param secondType The second type. + * @return The consistent type. + */ + private static int checkContentTypeConsistency(int firstType, int secondType) { + if (firstType == C.TRACK_TYPE_UNKNOWN) { + return secondType; + } else if (secondType == C.TRACK_TYPE_UNKNOWN) { + return firstType; + } else { + Assertions.checkState(firstType == secondType); + return firstType; + } + } + + /** + * Parses a {@link Descriptor} from an element. + * + * @param xpp The parser from which to read. + * @param tag The tag of the element being parsed. + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + * @return The parsed {@link Descriptor}. + */ + protected static Descriptor parseDescriptor(XmlPullParser xpp, String tag) + throws XmlPullParserException, IOException { + String schemeIdUri = parseString(xpp, "schemeIdUri", ""); + String value = parseString(xpp, "value", null); + String id = parseString(xpp, "id", null); + do { + xpp.next(); + } while (!XmlPullParserUtil.isEndTag(xpp, tag)); + return new Descriptor(schemeIdUri, value, id); + } + + protected static int parseCea608AccessibilityChannel( + List accessibilityDescriptors) { + for (int i = 0; i < accessibilityDescriptors.size(); i++) { + Descriptor descriptor = accessibilityDescriptors.get(i); + if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri) + && descriptor.value != null) { + Matcher accessibilityValueMatcher = CEA_608_ACCESSIBILITY_PATTERN.matcher(descriptor.value); + if (accessibilityValueMatcher.matches()) { + return Integer.parseInt(accessibilityValueMatcher.group(1)); + } else { + Log.w(TAG, "Unable to parse CEA-608 channel number from: " + descriptor.value); + } + } + } + return Format.NO_VALUE; + } + + protected static int parseCea708AccessibilityChannel( + List accessibilityDescriptors) { + for (int i = 0; i < accessibilityDescriptors.size(); i++) { + Descriptor descriptor = accessibilityDescriptors.get(i); + if ("urn:scte:dash:cc:cea-708:2015".equals(descriptor.schemeIdUri) + && descriptor.value != null) { + Matcher accessibilityValueMatcher = CEA_708_ACCESSIBILITY_PATTERN.matcher(descriptor.value); + if (accessibilityValueMatcher.matches()) { + return Integer.parseInt(accessibilityValueMatcher.group(1)); + } else { + Log.w(TAG, "Unable to parse CEA-708 service block number from: " + descriptor.value); + } + } + } + return Format.NO_VALUE; + } + + protected static String parseEac3SupplementalProperties(List supplementalProperties) { + for (int i = 0; i < supplementalProperties.size(); i++) { + Descriptor descriptor = supplementalProperties.get(i); + String schemeIdUri = descriptor.schemeIdUri; + if (("tag:dolby.com,2018:dash:EC3_ExtensionType:2018".equals(schemeIdUri) + && "JOC".equals(descriptor.value)) + || ("tag:dolby.com,2014:dash:DolbyDigitalPlusExtensionType:2014".equals(schemeIdUri) + && "ec+3".equals(descriptor.value))) { + return MimeTypes.AUDIO_E_AC3_JOC; + } + } + return MimeTypes.AUDIO_E_AC3; + } + + protected static float parseFrameRate(XmlPullParser xpp, float defaultValue) { + float frameRate = defaultValue; + String frameRateAttribute = xpp.getAttributeValue(null, "frameRate"); + if (frameRateAttribute != null) { + Matcher frameRateMatcher = FRAME_RATE_PATTERN.matcher(frameRateAttribute); + if (frameRateMatcher.matches()) { + int numerator = Integer.parseInt(frameRateMatcher.group(1)); + String denominatorString = frameRateMatcher.group(2); + if (!TextUtils.isEmpty(denominatorString)) { + frameRate = (float) numerator / Integer.parseInt(denominatorString); + } else { + frameRate = numerator; + } + } + } + return frameRate; + } + + protected static long parseDuration(XmlPullParser xpp, String name, long defaultValue) { + String value = xpp.getAttributeValue(null, name); + if (value == null) { + return defaultValue; + } else { + return Util.parseXsDuration(value); + } + } + + protected static long parseDateTime(XmlPullParser xpp, String name, long defaultValue) + throws ParserException { + String value = xpp.getAttributeValue(null, name); + if (value == null) { + return defaultValue; + } else { + return Util.parseXsDateTime(value); + } + } + + protected static String parseText(XmlPullParser xpp, String label) + throws XmlPullParserException, IOException { + String text = ""; + do { + xpp.next(); + if (xpp.getEventType() == XmlPullParser.TEXT) { + text = xpp.getText(); + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, label)); + return text; + } + + protected static int parseInt(XmlPullParser xpp, String name, int defaultValue) { + String value = xpp.getAttributeValue(null, name); + if (value == null || !TextUtils.isDigitsOnly(value)) { + return defaultValue; + } + return Integer.parseInt(value); + } + + protected static long parseLong(XmlPullParser xpp, String name, long defaultValue) { + String value = xpp.getAttributeValue(null, name); + boolean increase = false; + if (value !=null && value.contains(".")) { + value = value.split("\\.")[0]; + increase = true; + } + long longValue = (value == null) ? defaultValue : Long.parseLong(value); + if (increase) { + return longValue + 1; + } + return longValue; + } + + protected static float parseFloat(XmlPullParser xpp, String name, float defaultValue) { + String value = xpp.getAttributeValue(null, name); + return value == null ? defaultValue : Float.parseFloat(value); + } + + protected static String parseString(XmlPullParser xpp, String name, String defaultValue) { + String value = xpp.getAttributeValue(null, name); + return value == null ? defaultValue : value; + } + + /** + * Parses the number of channels from the value attribute of an AudioElementConfiguration with + * schemeIdUri "urn:mpeg:mpegB:cicp:ChannelConfiguration", as defined by ISO 23001-8 clause 8.1. + * + * @param xpp The parser from which to read. + * @return The parsed number of channels, or {@link Format#NO_VALUE} if the channel count could + * not be parsed. + */ + protected static int parseMpegChannelConfiguration(XmlPullParser xpp) { + int index = parseInt(xpp, "value", C.INDEX_UNSET); + return 0 <= index && index < MPEG_CHANNEL_CONFIGURATION_MAPPING.length + ? MPEG_CHANNEL_CONFIGURATION_MAPPING[index] + : Format.NO_VALUE; + } + + /** + * Parses the number of channels from the value attribute of an AudioElementConfiguration with + * schemeIdUri "tag:dolby.com,2014:dash:audio_channel_configuration:2011", as defined by table E.5 + * in ETSI TS 102 366, or the legacy schemeIdUri + * "urn:dolby:dash:audio_channel_configuration:2011". + * + * @param xpp The parser from which to read. + * @return The parsed number of channels, or {@link Format#NO_VALUE} if the channel count could + * not be parsed. + */ + protected static int parseDolbyChannelConfiguration(XmlPullParser xpp) { + String value = Util.toLowerInvariant(xpp.getAttributeValue(null, "value")); + if (value == null) { + return Format.NO_VALUE; + } + switch (value) { + case "4000": + return 1; + case "a000": + return 2; + case "f801": + return 6; + case "fa01": + return 8; + default: + return Format.NO_VALUE; + } + } + + protected static long parseLastSegmentNumberSupplementalProperty( + List supplementalProperties) { + for (int i = 0; i < supplementalProperties.size(); i++) { + Descriptor descriptor = supplementalProperties.get(i); + if ("http://dashif.org/guidelines/last-segment-number" + .equalsIgnoreCase(descriptor.schemeIdUri)) { + return Long.parseLong(descriptor.value); + } + } + return C.INDEX_UNSET; + } + + private static long getFinalAvailabilityTimeOffset( + long baseUrlAvailabilityTimeOffsetUs, long segmentBaseAvailabilityTimeOffsetUs) { + long availabilityTimeOffsetUs = segmentBaseAvailabilityTimeOffsetUs; + if (availabilityTimeOffsetUs == C.TIME_UNSET) { + // Fall back to BaseURL values if no SegmentBase specifies an offset. + availabilityTimeOffsetUs = baseUrlAvailabilityTimeOffsetUs; + } + if (availabilityTimeOffsetUs == Long.MAX_VALUE) { + // Replace INF value with TIME_UNSET to specify that all segments are available immediately. + availabilityTimeOffsetUs = C.TIME_UNSET; + } + return availabilityTimeOffsetUs; + } + + /** A parsed Representation element. */ + protected static final class RepresentationInfo { + + public final CustomFormat format; + public final String baseUrl; + public final CustomSegmentBase segmentBase; + @Nullable public final String drmSchemeType; + public final ArrayList drmSchemeDatas; + public final ArrayList inbandEventStreams; + public final long revisionId; + + public RepresentationInfo( + CustomFormat format, + String baseUrl, + CustomSegmentBase segmentBase, + @Nullable String drmSchemeType, + ArrayList drmSchemeDatas, + ArrayList inbandEventStreams, + long revisionId) { + this.format = format; + this.baseUrl = baseUrl; + this.segmentBase = segmentBase; + this.drmSchemeType = drmSchemeType; + this.drmSchemeDatas = drmSchemeDatas; + this.inbandEventStreams = inbandEventStreams; + this.revisionId = revisionId; + } + + } + + + +} diff --git a/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomFormat.java b/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomFormat.java new file mode 100644 index 000000000..b0ea88605 --- /dev/null +++ b/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomFormat.java @@ -0,0 +1,1801 @@ +package com.kaltura.android.exoplayer2.dashmanifestparser; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; + +import com.kaltura.android.exoplayer2.C; +import com.kaltura.android.exoplayer2.drm.DrmInitData; +import com.kaltura.android.exoplayer2.drm.ExoMediaCrypto; +import com.kaltura.android.exoplayer2.drm.UnsupportedMediaCrypto; +import com.kaltura.android.exoplayer2.metadata.Metadata; +import com.kaltura.android.exoplayer2.util.Assertions; +import com.kaltura.android.exoplayer2.util.MimeTypes; +import com.kaltura.android.exoplayer2.util.Util; +import com.kaltura.android.exoplayer2.video.ColorInfo; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Represents a media format. + * + *

When building formats, populate all fields whose values are known and relevant to the type of + * format being constructed. For information about different types of format, see ExoPlayer's Supported formats page. + * + *

Fields commonly relevant to all formats

+ * + *
    + *
  • {@link #id} + *
  • {@link #label} + *
  • {@link #language} + *
  • {@link #selectionFlags} + *
  • {@link #roleFlags} + *
  • {@link #averageBitrate} + *
  • {@link #peakBitrate} + *
  • {@link #codecs} + *
  • {@link #metadata} + *
+ * + *

Fields relevant to container formats

+ * + *
    + *
  • {@link #containerMimeType} + *
  • If the container only contains a single media track, fields + * relevant to sample formats can are also be relevant and can be set to describe the + * sample format of that track. + *
  • If the container only contains one track of a given type (possibly alongside tracks of + * other types), then fields relevant to that track type can be set to describe the properties + * of the track. See the sections below for video, audio and text formats. + *
+ * + *

Fields relevant to sample formats

+ * + *
    + *
  • {@link #sampleMimeType} + *
  • {@link #maxInputSize} + *
  • {@link #initializationData} + *
  • {@link #drmInitData} + *
  • {@link #subsampleOffsetUs} + *
  • Fields relevant to the sample format's track type are also relevant. See the sections below + * for video, audio and text formats. + *
+ * + *

Fields relevant to video formats

+ * + *
    + *
  • {@link #width} + *
  • {@link #height} + *
  • {@link #frameRate} + *
  • {@link #rotationDegrees} + *
  • {@link #pixelWidthHeightRatio} + *
  • {@link #projectionData} + *
  • {@link #stereoMode} + *
  • {@link #colorInfo} + *
+ * + *

Fields relevant to audio formats

+ * + *
    + *
  • {@link #channelCount} + *
  • {@link #sampleRate} + *
  • {@link #pcmEncoding} + *
  • {@link #encoderDelay} + *
  • {@link #encoderPadding} + *
+ * + *

Fields relevant to text formats

+ * + *
    + *
  • {@link #accessibilityChannel} + *
+ */ +public final class CustomFormat implements Parcelable { + + /** + * Builds {@link CustomFormat} instances. + * + *

Use CustomFormat#buildUpon() to obtain a builder representing an existing {@link CustomFormat}. + * + *

When building formats, populate all fields whose values are known and relevant to the type + * of format being constructed. See the {@link CustomFormat} Javadoc for information about which fields + * should be set for different types of format. + */ + public static final class Builder { + + @Nullable private String id; + @Nullable private String label; + @Nullable private String language; + @C.SelectionFlags private int selectionFlags; + @C.RoleFlags private int roleFlags; + private int averageBitrate; + private int peakBitrate; + @Nullable private String codecs; + @Nullable private Metadata metadata; + @Nullable FormatThumbnailInfo formatThumbnailInfo; + // Container specific. + + @Nullable private String containerMimeType; + + // Sample specific. + + @Nullable private String sampleMimeType; + private int maxInputSize; + @Nullable private List initializationData; + @Nullable private DrmInitData drmInitData; + private long subsampleOffsetUs; + + // Video specific. + + private int width; + private int height; + private float frameRate; + private int rotationDegrees; + private float pixelWidthHeightRatio; + @Nullable private byte[] projectionData; + @C.StereoMode private int stereoMode; + @Nullable private ColorInfo colorInfo; + + // Audio specific. + + private int channelCount; + private int sampleRate; + @C.PcmEncoding private int pcmEncoding; + private int encoderDelay; + private int encoderPadding; + + // Text specific. + + private int accessibilityChannel; + + // Provided by the source. + + @Nullable private Class exoMediaCryptoType; + + /** Creates a new instance with default values. */ + public Builder() { + averageBitrate = NO_VALUE; + peakBitrate = NO_VALUE; + // Sample specific. + maxInputSize = NO_VALUE; + subsampleOffsetUs = OFFSET_SAMPLE_RELATIVE; + // Video specific. + width = NO_VALUE; + height = NO_VALUE; + frameRate = NO_VALUE; + pixelWidthHeightRatio = 1.0f; + stereoMode = NO_VALUE; + // Audio specific. + channelCount = NO_VALUE; + sampleRate = NO_VALUE; + pcmEncoding = NO_VALUE; + // Text specific. + accessibilityChannel = NO_VALUE; + } + + /** + * Creates a new instance to build upon the provided {@link CustomFormat}. + * + * @param format The {@link CustomFormat} to build upon. + */ + private Builder(CustomFormat format) { + this.id = format.id; + this.label = format.label; + this.language = format.language; + this.selectionFlags = format.selectionFlags; + this.roleFlags = format.roleFlags; + this.averageBitrate = format.averageBitrate; + this.peakBitrate = format.peakBitrate; + this.codecs = format.codecs; + this.metadata = format.metadata; + // Container specific. + this.containerMimeType = format.containerMimeType; + // Sample specific. + this.sampleMimeType = format.sampleMimeType; + this.maxInputSize = format.maxInputSize; + this.initializationData = format.initializationData; + this.drmInitData = format.drmInitData; + this.subsampleOffsetUs = format.subsampleOffsetUs; + // Video specific. + this.width = format.width; + this.height = format.height; + this.frameRate = format.frameRate; + this.rotationDegrees = format.rotationDegrees; + this.pixelWidthHeightRatio = format.pixelWidthHeightRatio; + this.projectionData = format.projectionData; + this.stereoMode = format.stereoMode; + this.colorInfo = format.colorInfo; + // Audio specific. + this.channelCount = format.channelCount; + this.sampleRate = format.sampleRate; + this.pcmEncoding = format.pcmEncoding; + this.encoderDelay = format.encoderDelay; + this.encoderPadding = format.encoderPadding; + // Text specific. + this.accessibilityChannel = format.accessibilityChannel; + // Provided by the source. + this.exoMediaCryptoType = format.exoMediaCryptoType; + } + + /** + * Sets {@link CustomFormat#id}. The default value is {@code null}. + * + * @param id The {@link CustomFormat#id}. + * @return The builder. + */ + public Builder setId(@Nullable String id) { + this.id = id; + return this; + } + + public Builder setFormatThumbnailInfo(@Nullable FormatThumbnailInfo formatThumbnailInfo) { + this.formatThumbnailInfo = formatThumbnailInfo; + return this; + } + + /** + * Sets {@link CustomFormat#id} to {@link Integer#toString() Integer.toString(id)}. The default value + * is {@code null}. + * + * @param id The {@link CustomFormat#id}. + * @return The builder. + */ + public Builder setId(int id) { + this.id = Integer.toString(id); + return this; + } + + /** + * Sets {@link CustomFormat#label}. The default value is {@code null}. + * + * @param label The {@link CustomFormat#label}. + * @return The builder. + */ + public Builder setLabel(@Nullable String label) { + this.label = label; + return this; + } + + /** + * Sets {@link CustomFormat#language}. The default value is {@code null}. + * + * @param language The {@link CustomFormat#language}. + * @return The builder. + */ + public Builder setLanguage(@Nullable String language) { + this.language = language; + return this; + } + + /** + * Sets {@link CustomFormat#selectionFlags}. The default value is 0. + * + * @param selectionFlags The {@link CustomFormat#selectionFlags}. + * @return The builder. + */ + public Builder setSelectionFlags(@C.SelectionFlags int selectionFlags) { + this.selectionFlags = selectionFlags; + return this; + } + + /** + * Sets {@link CustomFormat#roleFlags}. The default value is 0. + * + * @param roleFlags The {@link CustomFormat#roleFlags}. + * @return The builder. + */ + public Builder setRoleFlags(@C.RoleFlags int roleFlags) { + this.roleFlags = roleFlags; + return this; + } + + /** + * Sets {@link CustomFormat#averageBitrate}. The default value is {@link #NO_VALUE}. + * + * @param averageBitrate The {@link CustomFormat#averageBitrate}. + * @return The builder. + */ + public Builder setAverageBitrate(int averageBitrate) { + this.averageBitrate = averageBitrate; + return this; + } + + /** + * Sets {@link CustomFormat#peakBitrate}. The default value is {@link #NO_VALUE}. + * + * @param peakBitrate The {@link CustomFormat#peakBitrate}. + * @return The builder. + */ + public Builder setPeakBitrate(int peakBitrate) { + this.peakBitrate = peakBitrate; + return this; + } + + /** + * Sets {@link CustomFormat#codecs}. The default value is {@code null}. + * + * @param codecs The {@link CustomFormat#codecs}. + * @return The builder. + */ + public Builder setCodecs(@Nullable String codecs) { + this.codecs = codecs; + return this; + } + + /** + * Sets {@link CustomFormat#metadata}. The default value is {@code null}. + * + * @param metadata The {@link CustomFormat#metadata}. + * @return The builder. + */ + public Builder setMetadata(@Nullable Metadata metadata) { + this.metadata = metadata; + return this; + } + + // Container specific. + + /** + * Sets {@link CustomFormat#containerMimeType}. The default value is {@code null}. + * + * @param containerMimeType The {@link CustomFormat#containerMimeType}. + * @return The builder. + */ + public Builder setContainerMimeType(@Nullable String containerMimeType) { + this.containerMimeType = containerMimeType; + return this; + } + + // Sample specific. + + /** + * Sets {@link CustomFormat#sampleMimeType}. The default value is {@code null}. + * + * @param sampleMimeType {@link CustomFormat#sampleMimeType}. + * @return The builder. + */ + public Builder setSampleMimeType(@Nullable String sampleMimeType) { + this.sampleMimeType = sampleMimeType; + return this; + } + + /** + * Sets {@link CustomFormat#maxInputSize}. The default value is {@link #NO_VALUE}. + * + * @param maxInputSize The {@link CustomFormat#maxInputSize}. + * @return The builder. + */ + public Builder setMaxInputSize(int maxInputSize) { + this.maxInputSize = maxInputSize; + return this; + } + + /** + * Sets {@link CustomFormat#initializationData}. The default value is {@code null}. + * + * @param initializationData The {@link CustomFormat#initializationData}. + * @return The builder. + */ + public Builder setInitializationData(@Nullable List initializationData) { + this.initializationData = initializationData; + return this; + } + + /** + * Sets {@link CustomFormat#drmInitData}. The default value is {@code null}. + * + * @param drmInitData The {@link CustomFormat#drmInitData}. + * @return The builder. + */ + public Builder setDrmInitData(@Nullable DrmInitData drmInitData) { + this.drmInitData = drmInitData; + return this; + } + + /** + * Sets {@link CustomFormat#subsampleOffsetUs}. The default value is {@link #OFFSET_SAMPLE_RELATIVE}. + * + * @param subsampleOffsetUs The {@link CustomFormat#subsampleOffsetUs}. + * @return The builder. + */ + public Builder setSubsampleOffsetUs(long subsampleOffsetUs) { + this.subsampleOffsetUs = subsampleOffsetUs; + return this; + } + + // Video specific. + + /** + * Sets {@link CustomFormat#width}. The default value is {@link #NO_VALUE}. + * + * @param width The {@link CustomFormat#width}. + * @return The builder. + */ + public Builder setWidth(int width) { + this.width = width; + return this; + } + + /** + * Sets {@link CustomFormat#height}. The default value is {@link #NO_VALUE}. + * + * @param height The {@link CustomFormat#height}. + * @return The builder. + */ + public Builder setHeight(int height) { + this.height = height; + return this; + } + + /** + * Sets {@link CustomFormat#frameRate}. The default value is {@link #NO_VALUE}. + * + * @param frameRate The {@link CustomFormat#frameRate}. + * @return The builder. + */ + public Builder setFrameRate(float frameRate) { + this.frameRate = frameRate; + return this; + } + + /** + * Sets {@link CustomFormat#rotationDegrees}. The default value is 0. + * + * @param rotationDegrees The {@link CustomFormat#rotationDegrees}. + * @return The builder. + */ + public Builder setRotationDegrees(int rotationDegrees) { + this.rotationDegrees = rotationDegrees; + return this; + } + + /** + * Sets {@link CustomFormat#pixelWidthHeightRatio}. The default value is 1.0f. + * + * @param pixelWidthHeightRatio The {@link CustomFormat#pixelWidthHeightRatio}. + * @return The builder. + */ + public Builder setPixelWidthHeightRatio(float pixelWidthHeightRatio) { + this.pixelWidthHeightRatio = pixelWidthHeightRatio; + return this; + } + + /** + * Sets {@link CustomFormat#projectionData}. The default value is {@code null}. + * + * @param projectionData The {@link CustomFormat#projectionData}. + * @return The builder. + */ + public Builder setProjectionData(@Nullable byte[] projectionData) { + this.projectionData = projectionData; + return this; + } + + /** + * Sets {@link CustomFormat#stereoMode}. The default value is {@link #NO_VALUE}. + * + * @param stereoMode The {@link CustomFormat#stereoMode}. + * @return The builder. + */ + public Builder setStereoMode(@C.StereoMode int stereoMode) { + this.stereoMode = stereoMode; + return this; + } + + /** + * Sets {@link CustomFormat#colorInfo}. The default value is {@code null}. + * + * @param colorInfo The {@link CustomFormat#colorInfo}. + * @return The builder. + */ + public Builder setColorInfo(@Nullable ColorInfo colorInfo) { + this.colorInfo = colorInfo; + return this; + } + + // Audio specific. + + /** + * Sets {@link CustomFormat#channelCount}. The default value is {@link #NO_VALUE}. + * + * @param channelCount The {@link CustomFormat#channelCount}. + * @return The builder. + */ + public Builder setChannelCount(int channelCount) { + this.channelCount = channelCount; + return this; + } + + /** + * Sets {@link CustomFormat#sampleRate}. The default value is {@link #NO_VALUE}. + * + * @param sampleRate The {@link CustomFormat#sampleRate}. + * @return The builder. + */ + public Builder setSampleRate(int sampleRate) { + this.sampleRate = sampleRate; + return this; + } + + /** + * Sets {@link CustomFormat#pcmEncoding}. The default value is {@link #NO_VALUE}. + * + * @param pcmEncoding The {@link CustomFormat#pcmEncoding}. + * @return The builder. + */ + public Builder setPcmEncoding(@C.PcmEncoding int pcmEncoding) { + this.pcmEncoding = pcmEncoding; + return this; + } + + /** + * Sets {@link CustomFormat#encoderDelay}. The default value is 0. + * + * @param encoderDelay The {@link CustomFormat#encoderDelay}. + * @return The builder. + */ + public Builder setEncoderDelay(int encoderDelay) { + this.encoderDelay = encoderDelay; + return this; + } + + /** + * Sets {@link CustomFormat#encoderPadding}. The default value is 0. + * + * @param encoderPadding The {@link CustomFormat#encoderPadding}. + * @return The builder. + */ + public Builder setEncoderPadding(int encoderPadding) { + this.encoderPadding = encoderPadding; + return this; + } + + // Text specific. + + /** + * Sets {@link CustomFormat#accessibilityChannel}. The default value is {@link #NO_VALUE}. + * + * @param accessibilityChannel The {@link CustomFormat#accessibilityChannel}. + * @return The builder. + */ + public Builder setAccessibilityChannel(int accessibilityChannel) { + this.accessibilityChannel = accessibilityChannel; + return this; + } + + // Provided by source. + + /** + * Sets {@link CustomFormat#exoMediaCryptoType}. The default value is {@code null}. + * + * @param exoMediaCryptoType The {@link CustomFormat#exoMediaCryptoType}. + * @return The builder. + */ + public Builder setExoMediaCryptoType( + @Nullable Class exoMediaCryptoType) { + this.exoMediaCryptoType = exoMediaCryptoType; + return this; + } + + // Build. + + public CustomFormat build() { + return new CustomFormat(/* builder= */ this); + } + } + + /** A value for various fields to indicate that the field's value is unknown or not applicable. */ + public static final int NO_VALUE = -1; + + /** + * A value for {@link #subsampleOffsetUs} to indicate that subsample timestamps are relative to + * the timestamps of their parent samples. + */ + public static final long OFFSET_SAMPLE_RELATIVE = Long.MAX_VALUE; + + /** An identifier for the format, or null if unknown or not applicable. */ + @Nullable public final String id; + /** The human readable label, or null if unknown or not applicable. */ + @Nullable public final String label; + /** The language as an IETF BCP 47 conformant tag, or null if unknown or not applicable. */ + @Nullable public final String language; + /** Track selection flags. */ + @C.SelectionFlags public final int selectionFlags; + /** Track role flags. */ + @C.RoleFlags public final int roleFlags; + /** + * The average bitrate in bits per second, or {@link #NO_VALUE} if unknown or not applicable. The + * way in which this field is populated depends on the type of media to which the format + * corresponds: + * + *

    + *
  • DASH representations: Always {@link CustomFormat#NO_VALUE}. + *
  • HLS variants: The {@code AVERAGE-BANDWIDTH} attribute defined on the corresponding {@code + * EXT-X-STREAM-INF} tag in the master playlist, or {@link CustomFormat#NO_VALUE} if not present. + *
  • SmoothStreaming track elements: The {@code Bitrate} attribute defined on the + * corresponding {@code TrackElement} in the manifest, or {@link CustomFormat#NO_VALUE} if not + * present. + *
  • Progressive container formats: Often {@link CustomFormat#NO_VALUE}, but may be populated with + * the average bitrate of the container if known. + *
  • Sample formats: Often {@link CustomFormat#NO_VALUE}, but may be populated with the average + * bitrate of the stream of samples with type {@link #sampleMimeType} if known. Note that if + * {@link #sampleMimeType} is a compressed format (e.g., {@link MimeTypes#AUDIO_AAC}), then + * this bitrate is for the stream of still compressed samples. + *
+ */ + public final int averageBitrate; + /** + * The peak bitrate in bits per second, or {@link #NO_VALUE} if unknown or not applicable. The way + * in which this field is populated depends on the type of media to which the format corresponds: + * + *
    + *
  • DASH representations: The {@code @bandwidth} attribute of the corresponding {@code + * Representation} element in the manifest. + *
  • HLS variants: The {@code BANDWIDTH} attribute defined on the corresponding {@code + * EXT-X-STREAM-INF} tag. + *
  • SmoothStreaming track elements: Always {@link CustomFormat#NO_VALUE}. + *
  • Progressive container formats: Often {@link CustomFormat#NO_VALUE}, but may be populated with + * the peak bitrate of the container if known. + *
  • Sample formats: Often {@link CustomFormat#NO_VALUE}, but may be populated with the peak bitrate + * of the stream of samples with type {@link #sampleMimeType} if known. Note that if {@link + * #sampleMimeType} is a compressed format (e.g., {@link MimeTypes#AUDIO_AAC}), then this + * bitrate is for the stream of still compressed samples. + *
+ */ + public final int peakBitrate; + /** + * The bitrate in bits per second. This is the peak bitrate if known, or else the average bitrate + * if known, or else {@link CustomFormat#NO_VALUE}. Equivalent to: {@code peakBitrate != NO_VALUE ? + * peakBitrate : averageBitrate}. + */ + public final int bitrate; + /** Codecs of the format as described in RFC 6381, or null if unknown or not applicable. */ + @Nullable public final String codecs; + /** Metadata, or null if unknown or not applicable. */ + @Nullable public final Metadata metadata; + @Nullable public final FormatThumbnailInfo formatThumbnailInfo; + // Container specific. + + /** The mime type of the container, or null if unknown or not applicable. */ + @Nullable public final String containerMimeType; + + // Sample specific. + + /** The sample mime type, or null if unknown or not applicable. */ + @Nullable public final String sampleMimeType; + /** + * The maximum size of a buffer of data (typically one sample), or {@link #NO_VALUE} if unknown or + * not applicable. + */ + public final int maxInputSize; + /** + * Initialization data that must be provided to the decoder. Will not be null, but may be empty + * if initialization data is not required. + */ + public final List initializationData; + /** DRM initialization data if the stream is protected, or null otherwise. */ + @Nullable public final DrmInitData drmInitData; + + /** + * For samples that contain subsamples, this is an offset that should be added to subsample + * timestamps. A value of {@link #OFFSET_SAMPLE_RELATIVE} indicates that subsample timestamps are + * relative to the timestamps of their parent samples. + */ + public final long subsampleOffsetUs; + + // Video specific. + + /** + * The width of the video in pixels, or {@link #NO_VALUE} if unknown or not applicable. + */ + public final int width; + /** + * The height of the video in pixels, or {@link #NO_VALUE} if unknown or not applicable. + */ + public final int height; + /** + * The frame rate in frames per second, or {@link #NO_VALUE} if unknown or not applicable. + */ + public final float frameRate; + /** + * The clockwise rotation that should be applied to the video for it to be rendered in the correct + * orientation, or 0 if unknown or not applicable. Only 0, 90, 180 and 270 are supported. + */ + public final int rotationDegrees; + /** The width to height ratio of pixels in the video, or 1.0 if unknown or not applicable. */ + public final float pixelWidthHeightRatio; + /** The projection data for 360/VR video, or null if not applicable. */ + @Nullable public final byte[] projectionData; + /** + * 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}, {@link C#STEREO_MODE_STEREO_MESH}. + */ + @C.StereoMode public final int stereoMode; + /** The color metadata associated with the video, or null if not applicable. */ + @Nullable public final ColorInfo colorInfo; + + // Audio specific. + + /** + * The number of audio channels, or {@link #NO_VALUE} if unknown or not applicable. + */ + public final int channelCount; + /** + * The audio sampling rate in Hz, or {@link #NO_VALUE} if unknown or not applicable. + */ + public final int sampleRate; + /** The {@link C.PcmEncoding} for PCM audio. Set to {@link #NO_VALUE} for other media types. */ + @C.PcmEncoding public final int pcmEncoding; + /** + * The number of frames to trim from the start of the decoded audio stream, or 0 if not + * applicable. + */ + public final int encoderDelay; + /** + * The number of frames to trim from the end of the decoded audio stream, or 0 if not applicable. + */ + public final int encoderPadding; + + // Text specific. + + /** The Accessibility channel, or {@link #NO_VALUE} if not known or applicable. */ + public final int accessibilityChannel; + + // Provided by source. + + /** + * The type of {@link ExoMediaCrypto} that will be associated with the content this format + * describes, or {@code null} if the content is not encrypted. Cannot be null if {@link + * #drmInitData} is non-null. + */ + @Nullable public final Class exoMediaCryptoType; + + // Lazily initialized hashcode. + private int hashCode; + + // Video. + + /** @deprecated Use {@link CustomFormat.Builder}. */ + @Deprecated + public static CustomFormat createVideoContainerFormat( + @Nullable String id, + @Nullable String label, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + @Nullable Metadata metadata, + @Nullable FormatThumbnailInfo formatThumbnailInfo, + int bitrate, + int width, + int height, + float frameRate, + @Nullable List initializationData, + @C.SelectionFlags int selectionFlags, + @C.RoleFlags int roleFlags) { + return new Builder() + .setId(id) + .setLabel(label) + .setSelectionFlags(selectionFlags) + .setRoleFlags(roleFlags) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setMetadata(metadata) + .setFormatThumbnailInfo(formatThumbnailInfo) + .setContainerMimeType(containerMimeType) + .setSampleMimeType(sampleMimeType) + .setInitializationData(initializationData) + .setWidth(width) + .setHeight(height) + .setFrameRate(frameRate) + .build(); + } + + /** @deprecated Use {@link CustomFormat.Builder}. */ + @Deprecated + public static CustomFormat createVideoSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int width, + int height, + float frameRate, + @Nullable List initializationData, + @Nullable DrmInitData drmInitData) { + return new Builder() + .setId(id) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setSampleMimeType(sampleMimeType) + .setMaxInputSize(maxInputSize) + .setInitializationData(initializationData) + .setDrmInitData(drmInitData) + .setWidth(width) + .setHeight(height) + .setFrameRate(frameRate) + .build(); + } + + /** @deprecated Use {@link CustomFormat.Builder}. */ + @Deprecated + public static CustomFormat createVideoSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int width, + int height, + float frameRate, + @Nullable List initializationData, + int rotationDegrees, + float pixelWidthHeightRatio, + @Nullable DrmInitData drmInitData) { + return new Builder() + .setId(id) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setSampleMimeType(sampleMimeType) + .setMaxInputSize(maxInputSize) + .setInitializationData(initializationData) + .setDrmInitData(drmInitData) + .setWidth(width) + .setHeight(height) + .setFrameRate(frameRate) + .setRotationDegrees(rotationDegrees) + .setPixelWidthHeightRatio(pixelWidthHeightRatio) + .build(); + } + + /** @deprecated Use {@link CustomFormat.Builder}. */ + @Deprecated + public static CustomFormat createVideoSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int width, + int height, + float frameRate, + @Nullable List initializationData, + int rotationDegrees, + float pixelWidthHeightRatio, + @Nullable byte[] projectionData, + @C.StereoMode int stereoMode, + @Nullable ColorInfo colorInfo, + @Nullable DrmInitData drmInitData) { + return new Builder() + .setId(id) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setSampleMimeType(sampleMimeType) + .setMaxInputSize(maxInputSize) + .setInitializationData(initializationData) + .setDrmInitData(drmInitData) + .setWidth(width) + .setHeight(height) + .setFrameRate(frameRate) + .setRotationDegrees(rotationDegrees) + .setPixelWidthHeightRatio(pixelWidthHeightRatio) + .setProjectionData(projectionData) + .setStereoMode(stereoMode) + .setColorInfo(colorInfo) + .build(); + } + + // Audio. + + /** @deprecated Use {@link CustomFormat.Builder}. */ + @Deprecated + public static CustomFormat createAudioContainerFormat( + @Nullable String id, + @Nullable String label, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + @Nullable Metadata metadata, + int bitrate, + int channelCount, + int sampleRate, + @Nullable List initializationData, + @C.SelectionFlags int selectionFlags, + @C.RoleFlags int roleFlags, + @Nullable String language) { + return new Builder() + .setId(id) + .setLabel(label) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setRoleFlags(roleFlags) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setMetadata(metadata) + .setContainerMimeType(containerMimeType) + .setSampleMimeType(sampleMimeType) + .setInitializationData(initializationData) + .setChannelCount(channelCount) + .setSampleRate(sampleRate) + .build(); + } + + /** @deprecated Use {@link CustomFormat.Builder}. */ + @Deprecated + public static CustomFormat createAudioSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int channelCount, + int sampleRate, + @Nullable List initializationData, + @Nullable DrmInitData drmInitData, + @C.SelectionFlags int selectionFlags, + @Nullable String language) { + return new Builder() + .setId(id) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setSampleMimeType(sampleMimeType) + .setMaxInputSize(maxInputSize) + .setInitializationData(initializationData) + .setDrmInitData(drmInitData) + .setChannelCount(channelCount) + .setSampleRate(sampleRate) + .build(); + } + + /** @deprecated Use {@link CustomFormat.Builder}. */ + @Deprecated + public static CustomFormat createAudioSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int channelCount, + int sampleRate, + @C.PcmEncoding int pcmEncoding, + @Nullable List initializationData, + @Nullable DrmInitData drmInitData, + @C.SelectionFlags int selectionFlags, + @Nullable String language) { + return new Builder() + .setId(id) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setSampleMimeType(sampleMimeType) + .setMaxInputSize(maxInputSize) + .setInitializationData(initializationData) + .setDrmInitData(drmInitData) + .setChannelCount(channelCount) + .setSampleRate(sampleRate) + .setPcmEncoding(pcmEncoding) + .build(); + } + + /** @deprecated Use {@link CustomFormat.Builder}. */ + @Deprecated + public static CustomFormat createAudioSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int channelCount, + int sampleRate, + @C.PcmEncoding int pcmEncoding, + int encoderDelay, + int encoderPadding, + @Nullable List initializationData, + @Nullable DrmInitData drmInitData, + @C.SelectionFlags int selectionFlags, + @Nullable String language, + @Nullable Metadata metadata) { + return new Builder() + .setId(id) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setMetadata(metadata) + .setSampleMimeType(sampleMimeType) + .setMaxInputSize(maxInputSize) + .setInitializationData(initializationData) + .setDrmInitData(drmInitData) + .setChannelCount(channelCount) + .setSampleRate(sampleRate) + .setPcmEncoding(pcmEncoding) + .setEncoderDelay(encoderDelay) + .setEncoderPadding(encoderPadding) + .build(); + } + + // Text. + + /** @deprecated Use {@link CustomFormat.Builder}. */ + @Deprecated + public static CustomFormat createTextContainerFormat( + @Nullable String id, + @Nullable String label, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + @C.SelectionFlags int selectionFlags, + @C.RoleFlags int roleFlags, + @Nullable String language) { + return new Builder() + .setId(id) + .setLabel(label) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setRoleFlags(roleFlags) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setContainerMimeType(containerMimeType) + .setSampleMimeType(sampleMimeType) + .build(); + } + + /** @deprecated Use {@link CustomFormat.Builder}. */ + @Deprecated + public static CustomFormat createTextContainerFormat( + @Nullable String id, + @Nullable String label, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + @C.SelectionFlags int selectionFlags, + @C.RoleFlags int roleFlags, + @Nullable String language, + int accessibilityChannel) { + return new Builder() + .setId(id) + .setLabel(label) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setRoleFlags(roleFlags) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setContainerMimeType(containerMimeType) + .setSampleMimeType(sampleMimeType) + .setAccessibilityChannel(accessibilityChannel) + .build(); + } + + /** @deprecated Use {@link CustomFormat.Builder}. */ + @Deprecated + public static CustomFormat createTextSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @C.SelectionFlags int selectionFlags, + @Nullable String language) { + return new Builder() + .setId(id) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setSampleMimeType(sampleMimeType) + .build(); + } + + /** @deprecated Use {@link CustomFormat.Builder}. */ + @Deprecated + public static CustomFormat createTextSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @C.SelectionFlags int selectionFlags, + @Nullable String language, + int accessibilityChannel, + long subsampleOffsetUs, + @Nullable List initializationData) { + return new Builder() + .setId(id) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setSampleMimeType(sampleMimeType) + .setInitializationData(initializationData) + .setSubsampleOffsetUs(subsampleOffsetUs) + .setAccessibilityChannel(accessibilityChannel) + .build(); + } + + // Image. + + /** @deprecated Use {@link CustomFormat.Builder}. */ + @Deprecated + public static CustomFormat createImageSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @C.SelectionFlags int selectionFlags, + @Nullable List initializationData, + @Nullable String language) { + return new Builder() + .setId(id) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setSampleMimeType(sampleMimeType) + .setInitializationData(initializationData) + .build(); + } + + // Generic. + + /** @deprecated Use {@link CustomFormat.Builder}. */ + @Deprecated + public static CustomFormat createContainerFormat( + @Nullable String id, + @Nullable String label, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + @C.SelectionFlags int selectionFlags, + @C.RoleFlags int roleFlags, + @Nullable String language) { + return new Builder() + .setId(id) + .setLabel(label) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setRoleFlags(roleFlags) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setContainerMimeType(containerMimeType) + .setSampleMimeType(sampleMimeType) + .build(); + } + + /** @deprecated Use {@link CustomFormat.Builder}. */ + @Deprecated + public static CustomFormat createSampleFormat(@Nullable String id, @Nullable String sampleMimeType) { + return new Builder().setId(id).setSampleMimeType(sampleMimeType).build(); + } + + private CustomFormat(Builder builder) { + id = builder.id; + label = builder.label; + language = Util.normalizeLanguageCode(builder.language); + selectionFlags = builder.selectionFlags; + roleFlags = builder.roleFlags; + averageBitrate = builder.averageBitrate; + peakBitrate = builder.peakBitrate; + bitrate = peakBitrate != NO_VALUE ? peakBitrate : averageBitrate; + codecs = builder.codecs; + metadata = builder.metadata; + formatThumbnailInfo = builder.formatThumbnailInfo; + // Container specific. + containerMimeType = builder.containerMimeType; + // Sample specific. + sampleMimeType = builder.sampleMimeType; + maxInputSize = builder.maxInputSize; + initializationData = + builder.initializationData == null ? Collections.emptyList() : builder.initializationData; + drmInitData = builder.drmInitData; + subsampleOffsetUs = builder.subsampleOffsetUs; + // Video specific. + width = builder.width; + height = builder.height; + frameRate = builder.frameRate; + rotationDegrees = builder.rotationDegrees == NO_VALUE ? 0 : builder.rotationDegrees; + pixelWidthHeightRatio = + builder.pixelWidthHeightRatio == NO_VALUE ? 1 : builder.pixelWidthHeightRatio; + projectionData = builder.projectionData; + stereoMode = builder.stereoMode; + colorInfo = builder.colorInfo; + // Audio specific. + channelCount = builder.channelCount; + sampleRate = builder.sampleRate; + pcmEncoding = builder.pcmEncoding; + encoderDelay = builder.encoderDelay == NO_VALUE ? 0 : builder.encoderDelay; + encoderPadding = builder.encoderPadding == NO_VALUE ? 0 : builder.encoderPadding; + // Text specific. + accessibilityChannel = builder.accessibilityChannel; + // Provided by source. + if (builder.exoMediaCryptoType == null && drmInitData != null) { + // Encrypted content must always have a non-null exoMediaCryptoType. + exoMediaCryptoType = UnsupportedMediaCrypto.class; + } else { + exoMediaCryptoType = builder.exoMediaCryptoType; + } + } + + // Some fields are deprecated but they're still assigned below. + @SuppressWarnings({"ResourceType"}) + /* package */ CustomFormat(Parcel in) { + id = in.readString(); + label = in.readString(); + language = in.readString(); + selectionFlags = in.readInt(); + roleFlags = in.readInt(); + averageBitrate = in.readInt(); + peakBitrate = in.readInt(); + bitrate = peakBitrate != NO_VALUE ? peakBitrate : averageBitrate; + codecs = in.readString(); + metadata = in.readParcelable(Metadata.class.getClassLoader()); + formatThumbnailInfo = in.readParcelable(FormatThumbnailInfo.class.getClassLoader()); + // Container specific. + containerMimeType = in.readString(); + // Sample specific. + sampleMimeType = in.readString(); + maxInputSize = in.readInt(); + int initializationDataSize = in.readInt(); + initializationData = new ArrayList<>(initializationDataSize); + for (int i = 0; i < initializationDataSize; i++) { + initializationData.add(Assertions.checkNotNull(in.createByteArray())); + } + drmInitData = in.readParcelable(DrmInitData.class.getClassLoader()); + subsampleOffsetUs = in.readLong(); + // Video specific. + width = in.readInt(); + height = in.readInt(); + frameRate = in.readFloat(); + rotationDegrees = in.readInt(); + pixelWidthHeightRatio = in.readFloat(); + boolean hasProjectionData = Util.readBoolean(in); + projectionData = hasProjectionData ? in.createByteArray() : null; + stereoMode = in.readInt(); + colorInfo = in.readParcelable(ColorInfo.class.getClassLoader()); + // Audio specific. + channelCount = in.readInt(); + sampleRate = in.readInt(); + pcmEncoding = in.readInt(); + encoderDelay = in.readInt(); + encoderPadding = in.readInt(); + // Text specific. + accessibilityChannel = in.readInt(); + // Provided by source. + // Encrypted content must always have a non-null exoMediaCryptoType. + exoMediaCryptoType = drmInitData != null ? UnsupportedMediaCrypto.class : null; + } + + /** Returns a {@link CustomFormat.Builder} initialized with the values of this instance. */ + public Builder buildUpon() { + return new Builder(this); + } + + /** @deprecated Use {@link #buildUpon()} and {@link Builder#setMaxInputSize(int)}. */ + @Deprecated + public CustomFormat copyWithMaxInputSize(int maxInputSize) { + return buildUpon().setMaxInputSize(maxInputSize).build(); + } + + /** @deprecated Use {@link #buildUpon()} and {@link Builder#setSubsampleOffsetUs(long)}. */ + @Deprecated + public CustomFormat copyWithSubsampleOffsetUs(long subsampleOffsetUs) { + return buildUpon().setSubsampleOffsetUs(subsampleOffsetUs).build(); + } + + /** @deprecated Use {@link #buildUpon()} and {@link Builder#setLabel(String)} . */ + @Deprecated + public CustomFormat copyWithLabel(@Nullable String label) { + return buildUpon().setLabel(label).build(); + } + + /** @deprecated Use {@link #withManifestFormatInfo(CustomFormat)}. */ + @Deprecated + public CustomFormat copyWithManifestFormatInfo(CustomFormat manifestFormat) { + return withManifestFormatInfo(manifestFormat); + } + + @SuppressWarnings("ReferenceEquality") + public CustomFormat withManifestFormatInfo(CustomFormat manifestFormat) { + if (this == manifestFormat) { + // No need to copy from ourselves. + return this; + } + + int trackType = MimeTypes.getTrackType(sampleMimeType); + + // Use manifest value only. + @Nullable String id = manifestFormat.id; + + // Prefer manifest values, but fill in from sample format if missing. + @Nullable String label = manifestFormat.label != null ? manifestFormat.label : this.label; + @Nullable String language = this.language; + if ((trackType == C.TRACK_TYPE_TEXT || trackType == C.TRACK_TYPE_AUDIO) + && manifestFormat.language != null) { + language = manifestFormat.language; + } + + // Prefer sample format values, but fill in from manifest if missing. + int averageBitrate = + this.averageBitrate == NO_VALUE ? manifestFormat.averageBitrate : this.averageBitrate; + int peakBitrate = this.peakBitrate == NO_VALUE ? manifestFormat.peakBitrate : this.peakBitrate; + @Nullable String codecs = this.codecs; + if (codecs == null) { + // The manifest format may be muxed, so filter only codecs of this format's type. If we still + // have more than one codec then we're unable to uniquely identify which codec to fill in. + @Nullable String codecsOfType = Util.getCodecsOfType(manifestFormat.codecs, trackType); + if (Util.splitCodecs(codecsOfType).length == 1) { + codecs = codecsOfType; + } + } + + @Nullable + Metadata metadata = + this.metadata == null + ? manifestFormat.metadata + : this.metadata.copyWithAppendedEntriesFrom(manifestFormat.metadata); + + float frameRate = this.frameRate; + if (frameRate == NO_VALUE && trackType == C.TRACK_TYPE_VIDEO) { + frameRate = manifestFormat.frameRate; + } + + // Merge manifest and sample format values. + @C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags; + @C.RoleFlags int roleFlags = this.roleFlags | manifestFormat.roleFlags; + @Nullable + DrmInitData drmInitData = + DrmInitData.createSessionCreationData(manifestFormat.drmInitData, this.drmInitData); + + return buildUpon() + .setId(id) + .setLabel(label) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setRoleFlags(roleFlags) + .setAverageBitrate(averageBitrate) + .setPeakBitrate(peakBitrate) + .setCodecs(codecs) + .setMetadata(metadata) + .setDrmInitData(drmInitData) + .setFrameRate(frameRate) + .build(); + } + + /** + * @deprecated Use {@link #buildUpon()}, {@link Builder#setEncoderDelay(int)} and {@link + * Builder#setEncoderPadding(int)}. + */ + @Deprecated + public CustomFormat copyWithGaplessInfo(int encoderDelay, int encoderPadding) { + return buildUpon().setEncoderDelay(encoderDelay).setEncoderPadding(encoderPadding).build(); + } + + /** @deprecated Use {@link #buildUpon()} and {@link Builder#setFrameRate(float)}. */ + @Deprecated + public CustomFormat copyWithFrameRate(float frameRate) { + return buildUpon().setFrameRate(frameRate).build(); + } + + /** @deprecated Use {@link #buildUpon()} and {@link Builder#setDrmInitData(DrmInitData)}. */ + @Deprecated + public CustomFormat copyWithDrmInitData(@Nullable DrmInitData drmInitData) { + return buildUpon().setDrmInitData(drmInitData).build(); + } + + /** @deprecated Use {@link #buildUpon()} and {@link Builder#setMetadata(Metadata)}. */ + @Deprecated + public CustomFormat copyWithMetadata(@Nullable Metadata metadata) { + return buildUpon().setMetadata(metadata).build(); + } + + /** + * @deprecated Use {@link #buildUpon()} and {@link Builder#setAverageBitrate(int)} and {@link + * Builder#setPeakBitrate(int)}. + */ + @Deprecated + public CustomFormat copyWithBitrate(int bitrate) { + return buildUpon().setAverageBitrate(bitrate).setPeakBitrate(bitrate).build(); + } + + /** + * @deprecated Use {@link #buildUpon()}, {@link Builder#setWidth(int)} and {@link + * Builder#setHeight(int)}. + */ + @Deprecated + public CustomFormat copyWithVideoSize(int width, int height) { + return buildUpon().setWidth(width).setHeight(height).build(); + } + + /** Returns a copy of this format with the specified {@link #exoMediaCryptoType}. */ + public CustomFormat copyWithExoMediaCryptoType( + @Nullable Class exoMediaCryptoType) { + return buildUpon().setExoMediaCryptoType(exoMediaCryptoType).build(); + } + + /** + * Returns the number of pixels if this is a video format whose {@link #width} and {@link #height} + * are known, or {@link #NO_VALUE} otherwise + */ + public int getPixelCount() { + return width == NO_VALUE || height == NO_VALUE ? NO_VALUE : (width * height); + } + + @Override + public String toString() { + return "Format(" + + id + + ", " + + label + + ", " + + containerMimeType + + ", " + + sampleMimeType + + ", " + + codecs + + ", " + + bitrate + + ", " + + language + + ", [" + + width + + ", " + + height + + ", " + + frameRate + + "]" + + ", [" + + channelCount + + ", " + + sampleRate + + "])"; + } + + @Override + public int hashCode() { + if (hashCode == 0) { + // Some fields for which hashing is expensive are deliberately omitted. + int result = 17; + result = 31 * result + (id == null ? 0 : id.hashCode()); + result = 31 * result + (label != null ? label.hashCode() : 0); + result = 31 * result + (language == null ? 0 : language.hashCode()); + result = 31 * result + selectionFlags; + result = 31 * result + roleFlags; + result = 31 * result + averageBitrate; + result = 31 * result + peakBitrate; + result = 31 * result + (codecs == null ? 0 : codecs.hashCode()); + result = 31 * result + (metadata == null ? 0 : metadata.hashCode()); + // Container specific. + result = 31 * result + (containerMimeType == null ? 0 : containerMimeType.hashCode()); + // Sample specific. + result = 31 * result + (sampleMimeType == null ? 0 : sampleMimeType.hashCode()); + result = 31 * result + maxInputSize; + // [Omitted] initializationData. + // [Omitted] drmInitData. + result = 31 * result + (int) subsampleOffsetUs; + // Video specific. + result = 31 * result + width; + result = 31 * result + height; + result = 31 * result + Float.floatToIntBits(frameRate); + result = 31 * result + rotationDegrees; + result = 31 * result + Float.floatToIntBits(pixelWidthHeightRatio); + // [Omitted] projectionData. + result = 31 * result + stereoMode; + // [Omitted] colorInfo. + // Audio specific. + result = 31 * result + channelCount; + result = 31 * result + sampleRate; + result = 31 * result + pcmEncoding; + result = 31 * result + encoderDelay; + result = 31 * result + encoderPadding; + // Text specific. + result = 31 * result + accessibilityChannel; + // Provided by the source. + result = 31 * result + (exoMediaCryptoType == null ? 0 : exoMediaCryptoType.hashCode()); + hashCode = result; + } + return hashCode; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + CustomFormat other = (CustomFormat) obj; + if (hashCode != 0 && other.hashCode != 0 && hashCode != other.hashCode) { + return false; + } + // Field equality checks ordered by type, with the cheapest checks first. + return selectionFlags == other.selectionFlags + && roleFlags == other.roleFlags + && averageBitrate == other.averageBitrate + && peakBitrate == other.peakBitrate + && maxInputSize == other.maxInputSize + && subsampleOffsetUs == other.subsampleOffsetUs + && width == other.width + && height == other.height + && rotationDegrees == other.rotationDegrees + && stereoMode == other.stereoMode + && channelCount == other.channelCount + && sampleRate == other.sampleRate + && pcmEncoding == other.pcmEncoding + && encoderDelay == other.encoderDelay + && encoderPadding == other.encoderPadding + && accessibilityChannel == other.accessibilityChannel + && Float.compare(frameRate, other.frameRate) == 0 + && Float.compare(pixelWidthHeightRatio, other.pixelWidthHeightRatio) == 0 + && Util.areEqual(exoMediaCryptoType, other.exoMediaCryptoType) + && Util.areEqual(id, other.id) + && Util.areEqual(label, other.label) + && Util.areEqual(codecs, other.codecs) + && Util.areEqual(containerMimeType, other.containerMimeType) + && Util.areEqual(sampleMimeType, other.sampleMimeType) + && Util.areEqual(language, other.language) + && Arrays.equals(projectionData, other.projectionData) + && Util.areEqual(metadata, other.metadata) + && Util.areEqual(colorInfo, other.colorInfo) + && Util.areEqual(drmInitData, other.drmInitData) + && initializationDataEquals(other); + } + + /** + * Returns whether the {@link #initializationData}s belonging to this format and {@code other} are + * equal. + * + * @param other The other format whose {@link #initializationData} is being compared. + * @return Whether the {@link #initializationData}s belonging to this format and {@code other} are + * equal. + */ + public boolean initializationDataEquals(CustomFormat other) { + if (initializationData.size() != other.initializationData.size()) { + return false; + } + for (int i = 0; i < initializationData.size(); i++) { + if (!Arrays.equals(initializationData.get(i), other.initializationData.get(i))) { + return false; + } + } + return true; + } + + // Utility methods + + /** Returns a prettier {@link String} than {@link #toString()}, intended for logging. */ + public static String toLogString(@Nullable CustomFormat format) { + if (format == null) { + return "null"; + } + StringBuilder builder = new StringBuilder(); + builder.append("id=").append(format.id).append(", mimeType=").append(format.sampleMimeType); + if (format.bitrate != NO_VALUE) { + builder.append(", bitrate=").append(format.bitrate); + } + if (format.codecs != null) { + builder.append(", codecs=").append(format.codecs); + } + if (format.width != NO_VALUE && format.height != NO_VALUE) { + builder.append(", res=").append(format.width).append("x").append(format.height); + } + if (format.frameRate != NO_VALUE) { + builder.append(", fps=").append(format.frameRate); + } + if (format.channelCount != NO_VALUE) { + builder.append(", channels=").append(format.channelCount); + } + if (format.sampleRate != NO_VALUE) { + builder.append(", sample_rate=").append(format.sampleRate); + } + if (format.language != null) { + builder.append(", language=").append(format.language); + } + if (format.label != null) { + builder.append(", label=").append(format.label); + } + return builder.toString(); + } + + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(label); + dest.writeString(language); + dest.writeInt(selectionFlags); + dest.writeInt(roleFlags); + dest.writeInt(averageBitrate); + dest.writeInt(peakBitrate); + dest.writeString(codecs); + dest.writeParcelable(metadata, 0); + dest.writeParcelable(formatThumbnailInfo, 0); + // Container specific. + dest.writeString(containerMimeType); + // Sample specific. + dest.writeString(sampleMimeType); + dest.writeInt(maxInputSize); + int initializationDataSize = initializationData.size(); + dest.writeInt(initializationDataSize); + for (int i = 0; i < initializationDataSize; i++) { + dest.writeByteArray(initializationData.get(i)); + } + dest.writeParcelable(drmInitData, 0); + dest.writeLong(subsampleOffsetUs); + // Video specific. + dest.writeInt(width); + dest.writeInt(height); + dest.writeFloat(frameRate); + dest.writeInt(rotationDegrees); + dest.writeFloat(pixelWidthHeightRatio); + Util.writeBoolean(dest, projectionData != null); + if (projectionData != null) { + dest.writeByteArray(projectionData); + } + dest.writeInt(stereoMode); + dest.writeParcelable(colorInfo, flags); + // Audio specific. + dest.writeInt(channelCount); + dest.writeInt(sampleRate); + dest.writeInt(pcmEncoding); + dest.writeInt(encoderDelay); + dest.writeInt(encoderPadding); + // Text specific. + dest.writeInt(accessibilityChannel); + } + + public static final Creator CREATOR = new Creator() { + + @Override + public CustomFormat createFromParcel(Parcel in) { + return new CustomFormat(in); + } + + @Override + public CustomFormat[] newArray(int size) { + return new CustomFormat[size]; + } + + }; + + public static class FormatThumbnailInfo implements Parcelable { + public String structure; + public int tilesHorizontal; + public int tilesVertical; + public long presentationTimeOffset; + public long timeScale; + public long startNumber; + public long endNumber; + public String imageTemplateUrl; + public long segmentDuration; + + FormatThumbnailInfo(Parcel in) { + structure = in.readString(); + tilesHorizontal = in.readInt(); + tilesVertical = in.readInt(); + presentationTimeOffset = in.readLong(); + timeScale = in.readLong(); + startNumber = in.readLong(); + endNumber = in.readLong(); + imageTemplateUrl = in.readString(); + segmentDuration = in.readLong(); + } + + private FormatThumbnailInfo(Builder builder) { + structure = builder.structure; + tilesHorizontal = builder.tilesHorizontal; + tilesVertical = builder.tilesVertical; + presentationTimeOffset = builder.presentationTimeOffset; + timeScale = builder.timeScale; + startNumber = builder.startNumber; + endNumber = builder.endNumber; + imageTemplateUrl = builder.imageTemplateUrl; + segmentDuration = builder.segmentDuration; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(structure); + dest.writeInt(tilesHorizontal); + dest.writeInt(tilesVertical); + dest.writeLong(presentationTimeOffset); + dest.writeLong(timeScale); + dest.writeLong(startNumber); + dest.writeLong(endNumber); + dest.writeString(imageTemplateUrl); + dest.writeLong(segmentDuration); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public FormatThumbnailInfo createFromParcel(Parcel in) { + return new FormatThumbnailInfo(in); + } + + @Override + public FormatThumbnailInfo[] newArray(int size) { + return new FormatThumbnailInfo[size]; + } + }; + + public static class Builder { + + private String structure; + private int tilesHorizontal; + private int tilesVertical; + private long presentationTimeOffset; + private long timeScale; + private long startNumber; + private long endNumber; + private String imageTemplateUrl; + private long segmentDuration; + + public FormatThumbnailInfo.Builder setStructure(String structure) { + this.structure = structure; + return this; + } + + public FormatThumbnailInfo.Builder setTilesHorizontal(int tilesHorizontal) { + this.tilesHorizontal = tilesHorizontal; + return this; + } + + public FormatThumbnailInfo.Builder setTilesVertical(int tilesVertical) { + this.tilesVertical = tilesVertical; + return this; + } + + public FormatThumbnailInfo.Builder setPresentationTimeOffset(long presentationTimeOffset) { + this.presentationTimeOffset = presentationTimeOffset; + return this; + } + + public FormatThumbnailInfo.Builder setTimeScale(long timeScale) { + this.timeScale = timeScale; + return this; + } + + public FormatThumbnailInfo.Builder setStartNumber(long startNumber) { + this.startNumber = startNumber; + return this; + } + + public FormatThumbnailInfo.Builder setEndNumber(long endNumber) { + this.endNumber = endNumber; + return this; + } + + public FormatThumbnailInfo.Builder setImageTemplateUrl(String imageTemplateUrl) { + this.imageTemplateUrl = imageTemplateUrl; + return this; + } + + public FormatThumbnailInfo.Builder setSegmentDuration(long segmentDuration) { + this.segmentDuration = segmentDuration; + return this; + } + + public FormatThumbnailInfo build() { + return new FormatThumbnailInfo(this); + } + } + } +} diff --git a/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomPeriod.java b/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomPeriod.java new file mode 100644 index 000000000..413f7915f --- /dev/null +++ b/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomPeriod.java @@ -0,0 +1,96 @@ +package com.kaltura.android.exoplayer2.dashmanifestparser; + +import androidx.annotation.Nullable; +import com.kaltura.android.exoplayer2.C; +import com.kaltura.android.exoplayer2.source.dash.manifest.Descriptor; +import com.kaltura.android.exoplayer2.source.dash.manifest.EventStream; + +import java.util.Collections; +import java.util.List; + +/** + * Encapsulates media content components over a contiguous period of time. + */ +public class CustomPeriod { + + /** + * The period identifier, if one exists. + */ + @Nullable public final String id; + + /** + * The start time of the period in milliseconds. + */ + public final long startMs; + + /** + * The adaptation sets belonging to the period. + */ + public final List adaptationSets; + + /** + * The event stream belonging to the period. + */ + public final List eventStreams; + + /** The asset identifier for this period, if one exists */ + @Nullable public final Descriptor assetIdentifier; + + /** + * @param id The period identifier. May be null. + * @param startMs The start time of the period in milliseconds. + * @param adaptationSets The adaptation sets belonging to the period. + */ + public CustomPeriod(@Nullable String id, long startMs, List adaptationSets) { + this(id, startMs, adaptationSets, Collections.emptyList(), /* assetIdentifier= */ null); + } + + /** + * @param id The period identifier. May be null. + * @param startMs The start time of the period in milliseconds. + * @param adaptationSets The adaptation sets belonging to the period. + * @param eventStreams The {@link EventStream}s belonging to the period. + */ + public CustomPeriod(@Nullable String id, long startMs, List adaptationSets, + List eventStreams) { + this(id, startMs, adaptationSets, eventStreams, /* assetIdentifier= */ null); + } + + /** + * @param id The period identifier. May be null. + * @param startMs The start time of the period in milliseconds. + * @param adaptationSets The adaptation sets belonging to the period. + * @param eventStreams The {@link EventStream}s belonging to the period. + * @param assetIdentifier The asset identifier for this period + */ + public CustomPeriod( + @Nullable String id, + long startMs, + List adaptationSets, + List eventStreams, + @Nullable Descriptor assetIdentifier) { + this.id = id; + this.startMs = startMs; + this.adaptationSets = Collections.unmodifiableList(adaptationSets); + this.eventStreams = Collections.unmodifiableList(eventStreams); + this.assetIdentifier = assetIdentifier; + } + + /** + * Returns the index of the first adaptation set of a given type, or {@link C#INDEX_UNSET} if no + * adaptation set of the specified type exists. + * + * @param type An adaptation set type. + * @return The index of the first adaptation set of the specified type, or {@link C#INDEX_UNSET}. + */ + public int getAdaptationSetIndex(int type) { + int adaptationCount = adaptationSets.size(); + for (int i = 0; i < adaptationCount; i++) { + if (adaptationSets.get(i).type == type) { + return i; + } + } + return C.INDEX_UNSET; + } + +} diff --git a/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomRepresentation.java b/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomRepresentation.java new file mode 100644 index 000000000..331ee6ddd --- /dev/null +++ b/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomRepresentation.java @@ -0,0 +1,362 @@ +package com.kaltura.android.exoplayer2.dashmanifestparser; + + +import android.net.Uri; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import com.kaltura.android.exoplayer2.C; +import com.kaltura.android.exoplayer2.source.dash.DashSegmentIndex; +import com.kaltura.android.exoplayer2.source.dash.manifest.Descriptor; +import com.kaltura.android.exoplayer2.source.dash.manifest.RangedUri; + +import java.util.Collections; +import java.util.List; + +/** + * A DASH representation. + */ +public abstract class CustomRepresentation { + + /** + * A default value for {@link #revisionId}. + */ + public static final long REVISION_ID_DEFAULT = -1; + + /** + * Identifies the revision of the media contained within the representation. If the media can + * change over time (e.g. as a result of it being re-encoded), then this identifier can be set to + * uniquely identify the revision of the media. The timestamp at which the media was encoded is + * often a suitable. + */ + public final long revisionId; + /** + * The format of the representation. + */ + public final CustomFormat format; + /** + * The base URL of the representation. + */ + public final String baseUrl; + /** + * The offset of the presentation timestamps in the media stream relative to media time. + */ + public final long presentationTimeOffsetUs; + /** The in-band event streams in the representation. May be empty. */ + public final List inbandEventStreams; + + private final RangedUri initializationUri; + + /** + * Constructs a new instance. + * + * @param revisionId Identifies the revision of the content. + * @param format The format of the representation. + * @param baseUrl The base URL. + * @param segmentBase A segment base element for the representation. + * @return The constructed instance. + */ + public static CustomRepresentation newInstance( + long revisionId, CustomFormat format, String baseUrl, CustomSegmentBase segmentBase) { + return newInstance(revisionId, format, baseUrl, segmentBase, /* inbandEventStreams= */ null); + } + + /** + * Constructs a new instance. + * + * @param revisionId Identifies the revision of the content. + * @param format The format of the representation. + * @param baseUrl The base URL. + * @param segmentBase A segment base element for the representation. + * @param inbandEventStreams The in-band event streams in the representation. May be null. + * @return The constructed instance. + */ + public static CustomRepresentation newInstance( + long revisionId, + CustomFormat format, + String baseUrl, + CustomSegmentBase segmentBase, + @Nullable List inbandEventStreams) { + return newInstance( + revisionId, + format, + baseUrl, + segmentBase, + inbandEventStreams, + /* cacheKey= */ null); + } + + /** + * Constructs a new instance. + * + * @param revisionId Identifies the revision of the content. + * @param format The format of the representation. + * @param baseUrl The base URL of the representation. + * @param segmentBase A segment base element for the representation. + * @param inbandEventStreams The in-band event streams in the representation. May be null. + * @param cacheKey An optional key to be returned from {@link #getCacheKey()}, or null. This + * parameter is ignored if {@code segmentBase} consists of multiple segments. + * @return The constructed instance. + */ + public static CustomRepresentation newInstance( + long revisionId, + CustomFormat format, + String baseUrl, + CustomSegmentBase segmentBase, + @Nullable List inbandEventStreams, + @Nullable String cacheKey) { + if (segmentBase instanceof CustomSegmentBase.SingleSegmentBase) { + return new CustomRepresentation.SingleSegmentRepresentation( + revisionId, + format, + baseUrl, + (CustomSegmentBase.SingleSegmentBase) segmentBase, + inbandEventStreams, + cacheKey, + C.LENGTH_UNSET); + } else if (segmentBase instanceof CustomSegmentBase.MultiSegmentBase) { + return new CustomRepresentation.MultiSegmentRepresentation( + revisionId, format, baseUrl, (CustomSegmentBase.MultiSegmentBase) segmentBase, inbandEventStreams); + } else { + throw new IllegalArgumentException("segmentBase must be of type SingleSegmentBase or " + + "MultiSegmentBase"); + } + } + + private CustomRepresentation( + long revisionId, + CustomFormat format, + String baseUrl, + CustomSegmentBase segmentBase, + @Nullable List inbandEventStreams) { + this.revisionId = revisionId; + this.format = format; + this.baseUrl = baseUrl; + this.inbandEventStreams = + inbandEventStreams == null + ? Collections.emptyList() + : Collections.unmodifiableList(inbandEventStreams); + initializationUri = segmentBase.getInitialization(this); + presentationTimeOffsetUs = segmentBase.getPresentationTimeOffsetUs(); + } + + /** + * Returns a {@link RangedUri} defining the location of the representation's initialization data, + * or null if no initialization data exists. + */ + @Nullable + public RangedUri getInitializationUri() { + return initializationUri; + } + + /** + * Returns a {@link RangedUri} defining the location of the representation's segment index, or + * null if the representation provides an index directly. + */ + @Nullable + public abstract RangedUri getIndexUri(); + + /** Returns an index if the representation provides one directly, or null otherwise. */ + @Nullable + public abstract DashSegmentIndex getIndex(); + + /** Returns a cache key for the representation if set, or null. */ + @Nullable + public abstract String getCacheKey(); + + /** + * A DASH representation consisting of a single segment. + */ + public static class SingleSegmentRepresentation extends CustomRepresentation { + + /** + * The uri of the single segment. + */ + public final Uri uri; + + /** + * The content length, or {@link C#LENGTH_UNSET} if unknown. + */ + public final long contentLength; + + @Nullable private final String cacheKey; + @Nullable private final RangedUri indexUri; + @Nullable private final CustomSingleSegmentIndex segmentIndex; + + /** + * @param revisionId Identifies the revision of the content. + * @param format The format of the representation. + * @param uri The uri of the media. + * @param initializationStart The offset of the first byte of initialization data. + * @param initializationEnd The offset of the last byte of initialization data. + * @param indexStart The offset of the first byte of index data. + * @param indexEnd The offset of the last byte of index data. + * @param inbandEventStreams The in-band event streams in the representation. May be null. + * @param cacheKey An optional key to be returned from {@link #getCacheKey()}, or null. + * @param contentLength The content length, or {@link C#LENGTH_UNSET} if unknown. + */ + public static SingleSegmentRepresentation newInstance( + long revisionId, + CustomFormat format, + String uri, + long initializationStart, + long initializationEnd, + long indexStart, + long indexEnd, + List inbandEventStreams, + @Nullable String cacheKey, + long contentLength) { + RangedUri rangedUri = new RangedUri(null, initializationStart, + initializationEnd - initializationStart + 1); + CustomSegmentBase.SingleSegmentBase segmentBase = new CustomSegmentBase.SingleSegmentBase(rangedUri, 1, 0, indexStart, + indexEnd - indexStart + 1); + return new SingleSegmentRepresentation( + revisionId, format, uri, segmentBase, inbandEventStreams, cacheKey, contentLength); + } + + /** + * @param revisionId Identifies the revision of the content. + * @param format The format of the representation. + * @param baseUrl The base URL of the representation. + * @param segmentBase The segment base underlying the representation. + * @param inbandEventStreams The in-band event streams in the representation. May be null. + * @param cacheKey An optional key to be returned from {@link #getCacheKey()}, or null. + * @param contentLength The content length, or {@link C#LENGTH_UNSET} if unknown. + */ + public SingleSegmentRepresentation( + long revisionId, + CustomFormat format, + String baseUrl, + CustomSegmentBase.SingleSegmentBase segmentBase, + @Nullable List inbandEventStreams, + @Nullable String cacheKey, + long contentLength) { + super(revisionId, format, baseUrl, segmentBase, inbandEventStreams); + this.uri = Uri.parse(baseUrl); + this.indexUri = segmentBase.getIndex(); + this.cacheKey = cacheKey; + this.contentLength = contentLength; + // If we have an index uri then the index is defined externally, and we shouldn't return one + // directly. If we don't, then we can't do better than an index defining a single segment. + segmentIndex = indexUri != null ? null + : new CustomSingleSegmentIndex(new RangedUri(null, 0, contentLength)); + } + + @Override + @Nullable + public RangedUri getIndexUri() { + return indexUri; + } + + @Override + @Nullable + public DashSegmentIndex getIndex() { + return segmentIndex; + } + + @Override + @Nullable + public String getCacheKey() { + return cacheKey; + } + + } + + /** + * A DASH representation consisting of multiple segments. + */ + public static class MultiSegmentRepresentation extends CustomRepresentation + implements DashSegmentIndex { + + @VisibleForTesting /* package */ final CustomSegmentBase.MultiSegmentBase segmentBase; + + /** + * Creates the multi-segment Representation. + * + * @param revisionId Identifies the revision of the content. + * @param format The format of the representation. + * @param baseUrl The base URL of the representation. + * @param segmentBase The segment base underlying the representation. + * @param inbandEventStreams The in-band event streams in the representation. May be null. + */ + public MultiSegmentRepresentation( + long revisionId, + CustomFormat format, + String baseUrl, + CustomSegmentBase.MultiSegmentBase segmentBase, + @Nullable List inbandEventStreams) { + super(revisionId, format, baseUrl, segmentBase, inbandEventStreams); + this.segmentBase = segmentBase; + } + + @Override + @Nullable + public RangedUri getIndexUri() { + return null; + } + + @Override + public DashSegmentIndex getIndex() { + return this; + } + + @Override + @Nullable + public String getCacheKey() { + return null; + } + + // DashSegmentIndex implementation. + + @Override + public RangedUri getSegmentUrl(long segmentIndex) { + return segmentBase.getSegmentUrl(this, segmentIndex); + } + + @Override + public long getSegmentNum(long timeUs, long periodDurationUs) { + return segmentBase.getSegmentNum(timeUs, periodDurationUs); + } + + @Override + public long getTimeUs(long segmentIndex) { + return segmentBase.getSegmentTimeUs(segmentIndex); + } + + @Override + public long getDurationUs(long segmentIndex, long periodDurationUs) { + return segmentBase.getSegmentDurationUs(segmentIndex, periodDurationUs); + } + + @Override + public long getFirstSegmentNum() { + return segmentBase.getFirstSegmentNum(); + } + + @Override + public long getFirstAvailableSegmentNum(long periodDurationUs, long nowUnixTimeUs) { + return segmentBase.getFirstAvailableSegmentNum(periodDurationUs, nowUnixTimeUs); + } + + @Override + public int getSegmentCount(long periodDurationUs) { + return segmentBase.getSegmentCount(periodDurationUs); + } + + @Override + public int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) { + return segmentBase.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs); + } + + @Override + public long getNextSegmentAvailableTimeUs(long periodDurationUs, long nowUnixTimeUs) { + return segmentBase.getNextSegmentAvailableTimeUs(periodDurationUs, nowUnixTimeUs); + } + + @Override + public boolean isExplicit() { + return segmentBase.isExplicit(); + } + + } + +} diff --git a/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomSegmentBase.java b/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomSegmentBase.java new file mode 100644 index 000000000..3ee3b7b8a --- /dev/null +++ b/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomSegmentBase.java @@ -0,0 +1,563 @@ +package com.kaltura.android.exoplayer2.dashmanifestparser; + +import static com.kaltura.android.exoplayer2.source.dash.DashSegmentIndex.INDEX_UNBOUNDED; +import static java.lang.Math.max; +import static java.lang.Math.min; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import com.kaltura.android.exoplayer2.C; +import com.kaltura.android.exoplayer2.source.dash.DashSegmentIndex; +import com.kaltura.android.exoplayer2.source.dash.manifest.RangedUri; +import com.kaltura.android.exoplayer2.source.dash.manifest.Representation; +import com.kaltura.android.exoplayer2.source.dash.manifest.UrlTemplate; +import com.kaltura.android.exoplayer2.util.Util; +import java.util.List; + +/** + * An approximate representation of a CustomSegmentBase manifest element. + */ +public abstract class CustomSegmentBase { + + @Nullable /* package */ final RangedUri initialization; + /* package */ final long timescale; + /* package */ final long presentationTimeOffset; + + /** + * @param initialization A {@link RangedUri} corresponding to initialization data, if such data + * exists. + * @param timescale The timescale in units per second. + * @param presentationTimeOffset The presentation time offset. The value in seconds is the + * division of this value and {@code timescale}. + */ + public CustomSegmentBase( + @Nullable RangedUri initialization, long timescale, long presentationTimeOffset) { + this.initialization = initialization; + this.timescale = timescale; + this.presentationTimeOffset = presentationTimeOffset; + } + + @Nullable + public RangedUri getInitialization() { + return initialization; + } + + public long getTimescale() { + return timescale; + } + + public long getPresentationTimeOffset() { + return presentationTimeOffset; + } + + /** + * Returns the {@link RangedUri} defining the location of initialization data for a given + * representation, or null if no initialization data exists. + * + * @param representation The {@link Representation} for which initialization data is required. + * @return A {@link RangedUri} defining the location of the initialization data, or null. + */ + @Nullable + public RangedUri getInitialization(CustomRepresentation representation) { + return initialization; + } + + /** + * Returns the presentation time offset, in microseconds. + */ + public long getPresentationTimeOffsetUs() { + return Util.scaleLargeTimestamp(presentationTimeOffset, C.MICROS_PER_SECOND, timescale); + } + + /** + * A {@link CustomSegmentBase} that defines a single segment. + */ + public static class SingleSegmentBase extends CustomSegmentBase { + + /* package */ final long indexStart; + /* package */ final long indexLength; + + /** + * @param initialization A {@link RangedUri} corresponding to initialization data, if such data + * exists. + * @param timescale The timescale in units per second. + * @param presentationTimeOffset The presentation time offset. The value in seconds is the + * division of this value and {@code timescale}. + * @param indexStart The byte offset of the index data in the segment. + * @param indexLength The length of the index data in bytes. + */ + public SingleSegmentBase( + @Nullable RangedUri initialization, + long timescale, + long presentationTimeOffset, + long indexStart, + long indexLength) { + super(initialization, timescale, presentationTimeOffset); + this.indexStart = indexStart; + this.indexLength = indexLength; + } + + public long getIndexStart() { + return indexStart; + } + + public long getIndexLength() { + return indexLength; + } + + public SingleSegmentBase() { + this( + /* initialization= */ null, + /* timescale= */ 1, + /* presentationTimeOffset= */ 0, + /* indexStart= */ 0, + /* indexLength= */ 0); + } + + @Nullable + public RangedUri getIndex() { + return indexLength <= 0 + ? null + : new RangedUri(/* referenceUri= */ null, indexStart, indexLength); + } + + } + + /** + * A {@link CustomSegmentBase} that consists of multiple segments. + */ + public abstract static class MultiSegmentBase extends CustomSegmentBase { + + /* package */ final long startNumber; + /* package */ final long duration; + @Nullable /* package */ final List segmentTimeline; + private final long timeShiftBufferDepthUs; + private final long periodStartUnixTimeUs; + + /** + * Offset to the current realtime at which segments become available, in microseconds, or {@link + * C#TIME_UNSET} if all segments are available immediately. + * + *

Segments will be available once their end time ≤ currentRealTime + + * availabilityTimeOffset. + */ + @VisibleForTesting /* package */ final long availabilityTimeOffsetUs; + + /** + * @param initialization A {@link RangedUri} corresponding to initialization data, if such data + * exists. + * @param timescale The timescale in units per second. + * @param presentationTimeOffset The presentation time offset. The value in seconds is the + * division of this value and {@code timescale}. + * @param startNumber The sequence number of the first segment. + * @param duration The duration of each segment in the case of fixed duration segments. The + * value in seconds is the division of this value and {@code timescale}. If {@code + * segmentTimeline} is non-null then this parameter is ignored. + * @param segmentTimeline A segment timeline corresponding to the segments. If null, then + * segments are assumed to be of fixed duration as specified by the {@code duration} + * parameter. + * @param availabilityTimeOffsetUs The offset to the current realtime at which segments become + * available in microseconds, or {@link C#TIME_UNSET} if not applicable. + * @param timeShiftBufferDepthUs The time shift buffer depth in microseconds. + * @param periodStartUnixTimeUs The start of the enclosing period in microseconds since the Unix + * epoch. + */ + public MultiSegmentBase( + @Nullable RangedUri initialization, + long timescale, + long presentationTimeOffset, + long startNumber, + long duration, + @Nullable List segmentTimeline, + long availabilityTimeOffsetUs, + long timeShiftBufferDepthUs, + long periodStartUnixTimeUs) { + super(initialization, timescale, presentationTimeOffset); + this.startNumber = startNumber; + this.duration = duration; + this.segmentTimeline = segmentTimeline; + this.availabilityTimeOffsetUs = availabilityTimeOffsetUs; + this.timeShiftBufferDepthUs = timeShiftBufferDepthUs; + this.periodStartUnixTimeUs = periodStartUnixTimeUs; + } + + public long getStartNumber() { + return startNumber; + } + + public long getDuration() { + return duration; + } + + @Nullable + public List getSegmentTimeline() { + return segmentTimeline; + } + + public long getTimeShiftBufferDepthUs() { + return timeShiftBufferDepthUs; + } + + public long getPeriodStartUnixTimeUs() { + return periodStartUnixTimeUs; + } + + public long getAvailabilityTimeOffsetUs() { + return availabilityTimeOffsetUs; + } + + /** See {@link DashSegmentIndex#getSegmentNum(long, long)}. */ + public long getSegmentNum(long timeUs, long periodDurationUs) { + final long firstSegmentNum = getFirstSegmentNum(); + final long 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; + long segmentNum = startNumber + timeUs / durationUs; + // Ensure we stay within bounds. + return segmentNum < firstSegmentNum + ? firstSegmentNum + : segmentCount == INDEX_UNBOUNDED + ? segmentNum + : min(segmentNum, firstSegmentNum + segmentCount - 1); + } else { + // The index cannot be unbounded. Identify the segment using binary search. + long lowIndex = firstSegmentNum; + long highIndex = firstSegmentNum + segmentCount - 1; + while (lowIndex <= highIndex) { + long midIndex = lowIndex + (highIndex - lowIndex) / 2; + long midTimeUs = getSegmentTimeUs(midIndex); + if (midTimeUs < timeUs) { + lowIndex = midIndex + 1; + } else if (midTimeUs > timeUs) { + highIndex = midIndex - 1; + } else { + return midIndex; + } + } + return lowIndex == firstSegmentNum ? lowIndex : highIndex; + } + } + + /** See {@link DashSegmentIndex#getDurationUs(long, long)}. */ + public final long getSegmentDurationUs(long sequenceNumber, long periodDurationUs) { + if (segmentTimeline != null) { + long duration = segmentTimeline.get((int) (sequenceNumber - startNumber)).duration; + return (duration * C.MICROS_PER_SECOND) / timescale; + } else { + int segmentCount = getSegmentCount(periodDurationUs); + return segmentCount != INDEX_UNBOUNDED + && sequenceNumber == (getFirstSegmentNum() + segmentCount - 1) + ? (periodDurationUs - getSegmentTimeUs(sequenceNumber)) + : ((duration * C.MICROS_PER_SECOND) / timescale); + } + } + + /** See {@link DashSegmentIndex#getTimeUs(long)}. */ + public final long getSegmentTimeUs(long sequenceNumber) { + long unscaledSegmentTime; + if (segmentTimeline != null) { + unscaledSegmentTime = + segmentTimeline.get((int) (sequenceNumber - startNumber)).startTime + - presentationTimeOffset; + } else { + unscaledSegmentTime = (sequenceNumber - startNumber) * duration; + } + return Util.scaleLargeTimestamp(unscaledSegmentTime, C.MICROS_PER_SECOND, timescale); + } + + /** + * Returns a {@link RangedUri} defining the location of a segment for the given index in the + * given representation. + * + *

See {@link DashSegmentIndex#getSegmentUrl(long)}. + */ + public abstract RangedUri getSegmentUrl(CustomRepresentation representation, long index); + + /** See {@link DashSegmentIndex#getFirstSegmentNum()}. */ + public long getFirstSegmentNum() { + return startNumber; + } + + /** See {@link DashSegmentIndex#getFirstAvailableSegmentNum(long, long)}. */ + public long getFirstAvailableSegmentNum(long periodDurationUs, long nowUnixTimeUs) { + long segmentCount = getSegmentCount(periodDurationUs); + if (segmentCount != INDEX_UNBOUNDED || timeShiftBufferDepthUs == C.TIME_UNSET) { + return getFirstSegmentNum(); + } + // The index is itself unbounded. We need to use the current time to calculate the range of + // available segments. + long liveEdgeTimeInPeriodUs = nowUnixTimeUs - periodStartUnixTimeUs; + long timeShiftBufferStartInPeriodUs = liveEdgeTimeInPeriodUs - timeShiftBufferDepthUs; + long timeShiftBufferStartSegmentNum = + getSegmentNum(timeShiftBufferStartInPeriodUs, periodDurationUs); + return max(getFirstSegmentNum(), timeShiftBufferStartSegmentNum); + } + + /** See {@link DashSegmentIndex#getAvailableSegmentCount(long, long)}. */ + public int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) { + int segmentCount = getSegmentCount(periodDurationUs); + if (segmentCount != INDEX_UNBOUNDED) { + return segmentCount; + } + // The index is itself unbounded. We need to use the current time to calculate the range of + // available segments. + long liveEdgeTimeInPeriodUs = nowUnixTimeUs - periodStartUnixTimeUs; + long availabilityTimeOffsetUs = liveEdgeTimeInPeriodUs + this.availabilityTimeOffsetUs; + // getSegmentNum(availabilityTimeOffsetUs) will not be completed yet. + long firstIncompleteSegmentNum = getSegmentNum(availabilityTimeOffsetUs, periodDurationUs); + long firstAvailableSegmentNum = getFirstAvailableSegmentNum(periodDurationUs, nowUnixTimeUs); + return (int) (firstIncompleteSegmentNum - firstAvailableSegmentNum); + } + + /** See {@link DashSegmentIndex#getNextSegmentAvailableTimeUs(long, long)}. */ + public long getNextSegmentAvailableTimeUs(long periodDurationUs, long nowUnixTimeUs) { + if (segmentTimeline != null) { + return C.TIME_UNSET; + } + long firstIncompleteSegmentNum = + getFirstAvailableSegmentNum(periodDurationUs, nowUnixTimeUs) + + getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs); + return getSegmentTimeUs(firstIncompleteSegmentNum) + + getSegmentDurationUs(firstIncompleteSegmentNum, periodDurationUs) + - availabilityTimeOffsetUs; + } + + /** See {@link DashSegmentIndex#isExplicit()} */ + public boolean isExplicit() { + return segmentTimeline != null; + } + + /** See {@link DashSegmentIndex#getSegmentCount(long)}. */ + public abstract int getSegmentCount(long periodDurationUs); + } + + /** A {@link MultiSegmentBase} that uses a SegmentList to define its segments. */ + public static final class SegmentList extends CustomSegmentBase.MultiSegmentBase { + + @Nullable /* package */ final List mediaSegments; + + /** + * @param initialization A {@link RangedUri} corresponding to initialization data, if such data + * exists. + * @param timescale The timescale in units per second. + * @param presentationTimeOffset The presentation time offset. The value in seconds is the + * division of this value and {@code timescale}. + * @param startNumber The sequence number of the first segment. + * @param duration The duration of each segment in the case of fixed duration segments. The + * value in seconds is the division of this value and {@code timescale}. If {@code + * segmentTimeline} is non-null then this parameter is ignored. + * @param segmentTimeline A segment timeline corresponding to the segments. If null, then + * segments are assumed to be of fixed duration as specified by the {@code duration} + * parameter. + * @param availabilityTimeOffsetUs The offset to the current realtime at which segments become + * available in microseconds, or {@link C#TIME_UNSET} if not applicable. + * @param mediaSegments A list of {@link RangedUri}s indicating the locations of the segments. + * @param timeShiftBufferDepthUs The time shift buffer depth in microseconds. + * @param periodStartUnixTimeUs The start of the enclosing period in microseconds since the Unix + * epoch. + */ + public SegmentList( + RangedUri initialization, + long timescale, + long presentationTimeOffset, + long startNumber, + long duration, + @Nullable List segmentTimeline, + long availabilityTimeOffsetUs, + @Nullable List mediaSegments, + long timeShiftBufferDepthUs, + long periodStartUnixTimeUs) { + super( + initialization, + timescale, + presentationTimeOffset, + startNumber, + duration, + segmentTimeline, + availabilityTimeOffsetUs, + timeShiftBufferDepthUs, + periodStartUnixTimeUs); + this.mediaSegments = mediaSegments; + } + + @Override + public RangedUri getSegmentUrl(CustomRepresentation representation, long sequenceNumber) { + return mediaSegments.get((int) (sequenceNumber - startNumber)); + } + + @Override + public int getSegmentCount(long periodDurationUs) { + return mediaSegments.size(); + } + + @Override + public boolean isExplicit() { + return true; + } + + } + + /** A {@link MultiSegmentBase} that uses a SegmentTemplate to define its segments. */ + public static final class SegmentTemplate extends MultiSegmentBase { + + @Nullable /* package */ final UrlTemplate initializationTemplate; + @Nullable /* package */ final UrlTemplate mediaTemplate; + /* package */ final long endNumber; + + /** + * @param initialization A {@link RangedUri} corresponding to initialization data, if such data + * exists. The value of this parameter is ignored if {@code initializationTemplate} is + * non-null. + * @param timescale The timescale in units per second. + * @param presentationTimeOffset The presentation time offset. The value in seconds is the + * division of this value and {@code timescale}. + * @param startNumber The sequence number of the first segment. + * @param endNumber The sequence number of the last segment as specified by the + * SupplementalProperty with schemeIdUri="http://dashif.org/guidelines/last-segment-number", + * or {@link C#INDEX_UNSET}. + * @param duration The duration of each segment in the case of fixed duration segments. The + * value in seconds is the division of this value and {@code timescale}. If {@code + * segmentTimeline} is non-null then this parameter is ignored. + * @param segmentTimeline A segment timeline corresponding to the segments. If null, then + * segments are assumed to be of fixed duration as specified by the {@code duration} + * parameter. + * @param availabilityTimeOffsetUs The offset to the current realtime at which segments become + * available in microseconds, or {@link C#TIME_UNSET} if not applicable. + * @param initializationTemplate A template defining the location of initialization data, if + * such data exists. If non-null then the {@code initialization} parameter is ignored. If + * null then {@code initialization} will be used. + * @param mediaTemplate A template defining the location of each media segment. + * @param timeShiftBufferDepthUs The time shift buffer depth in microseconds. + * @param periodStartUnixTimeUs The start of the enclosing period in microseconds since the Unix + * epoch. + */ + public SegmentTemplate( + RangedUri initialization, + long timescale, + long presentationTimeOffset, + long startNumber, + long endNumber, + long duration, + @Nullable List segmentTimeline, + long availabilityTimeOffsetUs, + @Nullable UrlTemplate initializationTemplate, + @Nullable UrlTemplate mediaTemplate, + long timeShiftBufferDepthUs, + long periodStartUnixTimeUs) { + super( + initialization, + timescale, + presentationTimeOffset, + startNumber, + duration, + segmentTimeline, + availabilityTimeOffsetUs, + timeShiftBufferDepthUs, + periodStartUnixTimeUs); + this.initializationTemplate = initializationTemplate; + this.mediaTemplate = mediaTemplate; + this.endNumber = endNumber; + } + + @Nullable + public UrlTemplate getInitializationTemplate() { + return initializationTemplate; + } + + @Nullable + public UrlTemplate getMediaTemplate() { + return mediaTemplate; + } + + public long getEndNumber() { + return endNumber; + } + + @Override + @Nullable + public RangedUri getInitialization(CustomRepresentation representation) { + if (initializationTemplate != null) { + String urlString = initializationTemplate.buildUri(representation.format.id, 0, + representation.format.bitrate, 0); + return new RangedUri(urlString, 0, C.LENGTH_UNSET); + } else { + return super.getInitialization(representation); + } + } + + @Override + public RangedUri getSegmentUrl(CustomRepresentation representation, long sequenceNumber) { + long time; + if (segmentTimeline != null) { + time = segmentTimeline.get((int) (sequenceNumber - startNumber)).startTime; + } else { + time = (sequenceNumber - startNumber) * duration; + } + String uriString = mediaTemplate.buildUri(representation.format.id, sequenceNumber, + representation.format.bitrate, time); + return new RangedUri(uriString, 0, C.LENGTH_UNSET); + } + + @Override + public int getSegmentCount(long periodDurationUs) { + if (segmentTimeline != null) { + return segmentTimeline.size(); + } else if (endNumber != C.INDEX_UNSET) { + return (int) (endNumber - startNumber + 1); + } else if (periodDurationUs != C.TIME_UNSET) { + long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; + return (int) Util.ceilDivide(periodDurationUs, durationUs); + } else { + return INDEX_UNBOUNDED; + } + } + } + + /** Represents a timeline segment from the MPD's SegmentTimeline list. */ + public static final class SegmentTimelineElement { + + /* package */ final long startTime; + /* package */ final long duration; + + /** + * @param startTime The start time of the element. The value in seconds is the division of this + * value and the {@code timescale} of the enclosing element. + * @param duration The duration of the element. The value in seconds is the division of this + * value and the {@code timescale} of the enclosing element. + */ + public SegmentTimelineElement(long startTime, long duration) { + this.startTime = startTime; + this.duration = duration; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SegmentTimelineElement that = (SegmentTimelineElement) o; + return startTime == that.startTime && duration == that.duration; + } + + public long getStartTime() { + return startTime; + } + + public long getDuration() { + return duration; + } + + @Override + public int hashCode() { + return 31 * (int) startTime + (int) duration; + } + } + +} diff --git a/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomSingleSegmentIndex.java b/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomSingleSegmentIndex.java new file mode 100644 index 000000000..a1f73a530 --- /dev/null +++ b/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomSingleSegmentIndex.java @@ -0,0 +1,72 @@ +package com.kaltura.android.exoplayer2.dashmanifestparser; + + +import com.kaltura.android.exoplayer2.C; +import com.kaltura.android.exoplayer2.source.dash.DashSegmentIndex; +import com.kaltura.android.exoplayer2.source.dash.manifest.RangedUri; + +/** + * A {@link DashSegmentIndex} that defines a single segment. + */ +/* package */ final class CustomSingleSegmentIndex implements DashSegmentIndex { + + private final RangedUri uri; + + /** + * @param uri A {@link RangedUri} defining the location of the segment data. + */ + public CustomSingleSegmentIndex(RangedUri uri) { + this.uri = uri; + } + + @Override + public long getSegmentNum(long timeUs, long periodDurationUs) { + return 0; + } + + @Override + public long getTimeUs(long segmentNum) { + return 0; + } + + @Override + public long getDurationUs(long segmentNum, long periodDurationUs) { + return periodDurationUs; + } + + @Override + public RangedUri getSegmentUrl(long segmentNum) { + return uri; + } + + @Override + public long getFirstSegmentNum() { + return 0; + } + + @Override + public long getFirstAvailableSegmentNum(long periodDurationUs, long nowUnixTimeUs) { + return 0; + } + + @Override + public int getSegmentCount(long periodDurationUs) { + return 1; + } + + @Override + public int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) { + return 1; + } + + @Override + public long getNextSegmentAvailableTimeUs(long periodDurationUs, long nowUnixTimeUs) { + return C.TIME_UNSET; + } + + @Override + public boolean isExplicit() { + return true; + } + +} diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java index a57f7db01..328cb883c 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java @@ -15,6 +15,7 @@ import android.content.Context; import android.media.MediaCodec; import android.net.Uri; +import android.os.Build; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; @@ -33,6 +34,8 @@ import com.kaltura.android.exoplayer2.SimpleExoPlayer; import com.kaltura.android.exoplayer2.Timeline; import com.kaltura.android.exoplayer2.audio.AudioAttributes; +import com.kaltura.android.exoplayer2.dashmanifestparser.CustomDashManifest; +import com.kaltura.android.exoplayer2.dashmanifestparser.CustomDashManifestParser; import com.kaltura.android.exoplayer2.drm.DrmSessionManager; import com.kaltura.android.exoplayer2.drm.DrmSessionManagerProvider; import com.kaltura.android.exoplayer2.ext.okhttp.OkHttpDataSource; @@ -50,17 +53,20 @@ import com.kaltura.android.exoplayer2.source.TrackGroupArray; import com.kaltura.android.exoplayer2.source.dash.DashMediaSource; import com.kaltura.android.exoplayer2.source.dash.DefaultDashChunkSource; +import com.kaltura.android.exoplayer2.source.dash.manifest.DashManifest; import com.kaltura.android.exoplayer2.source.hls.HlsMediaSource; import com.kaltura.android.exoplayer2.trackselection.DefaultTrackSelector; import com.kaltura.android.exoplayer2.trackselection.TrackSelectionArray; import com.kaltura.android.exoplayer2.ui.SubtitleView; import com.kaltura.android.exoplayer2.upstream.BandwidthMeter; +import com.kaltura.android.exoplayer2.upstream.ByteArrayDataSink; import com.kaltura.android.exoplayer2.upstream.DataSource; import com.kaltura.android.exoplayer2.upstream.DefaultAllocator; import com.kaltura.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.kaltura.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.kaltura.android.exoplayer2.upstream.DefaultHttpDataSource; import com.kaltura.android.exoplayer2.upstream.HttpDataSource; +import com.kaltura.android.exoplayer2.upstream.TeeDataSource; import com.kaltura.android.exoplayer2.video.CustomLoadControl; import com.kaltura.playkit.*; @@ -71,10 +77,12 @@ import com.kaltura.playkit.utils.Consts; import com.kaltura.playkit.utils.NativeCookieJarBridge; +import java.io.IOException; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -90,6 +98,8 @@ public class ExoPlayerWrapper implements PlayerEngine, Player.EventListener, MetadataOutput, BandwidthMeter.EventListener { + private ByteArrayDataSink lastDataSink; + public interface LoadControlStrategy { LoadControl getCustomLoadControl(); BandwidthMeter getCustomBandwidthMeter(); @@ -367,7 +377,7 @@ private MediaItem buildExoMediaItem(PKMediaSourceConfig sourceConfig) { MediaItem.DrmConfiguration drmConfiguration = mediaItem.playbackProperties.drmConfiguration; if (!(sourceConfig.mediaSource instanceof LocalAssetsManager.LocalMediaSource) && - playerSettings.isForceWidevineL3Playback() && + drmConfiguration != null && drmConfiguration.licenseUri != null && !TextUtils.isEmpty(drmConfiguration.licenseUri.toString())) { @@ -410,16 +420,19 @@ private MediaSource buildInternalExoMediaSource(MediaItem mediaItem, PKMediaSour PKRequestParams requestParams = sourceConfig.getRequestParams(); if (requestParams.headers == null || requestParams.headers.isEmpty()) { - return null; + //return null; } final DataSource.Factory dataSourceFactory = getDataSourceFactory(requestParams.headers); + final DataSource.Factory teedDtaSourceFactory = () -> { + lastDataSink = new ByteArrayDataSink(); + return new TeeDataSource(dataSourceFactory.createDataSource(), lastDataSink); }; MediaSource mediaSource; switch (format) { case dash: mediaSource = new DashMediaSource.Factory( - new DefaultDashChunkSource.Factory(dataSourceFactory), dataSourceFactory) + new DefaultDashChunkSource.Factory(dataSourceFactory), teedDtaSourceFactory) .setDrmSessionManager(sourceConfig.mediaSource.hasDrmParams() ? drmSessionManager : DrmSessionManager.DRM_UNSUPPORTED) .createMediaSource(mediaItem); break; @@ -865,12 +878,28 @@ public void onPositionDiscontinuity(int reason) { public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { log.d("onTracksChanged"); - + String dashManifestString = ""; + //if onTracksChanged happened when application went background, do not update the tracks. if (assertTrackSelectionIsNotNull("onTracksChanged()")) { + //if the track info new -> map the available tracks. and when ready, notify user about available tracks. if (shouldGetTracksInfo) { - shouldGetTracksInfo = !trackSelectionHelper.prepareTracks(trackSelections); + CustomDashManifest customDashManifest = null; + if (player.getCurrentManifest() instanceof DashManifest) { + byte[] bytes = lastDataSink.getData(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + dashManifestString = new String(bytes, StandardCharsets.UTF_8); + + try { + customDashManifest = new CustomDashManifestParser().parse(player.getMediaItemAt(0).playbackProperties.uri ,dashManifestString); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + shouldGetTracksInfo = !trackSelectionHelper.prepareTracks(trackSelections, customDashManifest); } trackSelectionHelper.notifyAboutTrackChange(trackSelections); @@ -1020,6 +1049,7 @@ public long getProgramStartTime() { @Override public void seekTo(long position) { log.v("seekTo"); + if (assertPlayerIsNotNull("seekTo()")) { isSeeking = true; sendDistinctEvent(PlayerEvent.Type.SEEKING); diff --git a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java index 2bcbd4efb..17a13c1ed 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java @@ -21,6 +21,7 @@ import com.kaltura.android.exoplayer2.Format; import com.kaltura.android.exoplayer2.RendererCapabilities; +import com.kaltura.android.exoplayer2.dashmanifestparser.CustomDashManifest; import com.kaltura.android.exoplayer2.source.TrackGroup; import com.kaltura.android.exoplayer2.source.TrackGroupArray; import com.kaltura.android.exoplayer2.trackselection.DefaultTrackSelector; @@ -157,7 +158,7 @@ enum TrackType { * * @return - true if tracks data created successful, if mappingTrackInfo not ready return false. */ - boolean prepareTracks(TrackSelectionArray trackSelections) { + boolean prepareTracks(TrackSelectionArray trackSelections, CustomDashManifest customDashManifest) { trackSelectionArray = trackSelections; mappedTrackInfo = selector.getCurrentMappedTrackInfo(); if (mappedTrackInfo == null) { From 0bd1909c4c1054138b66099060b4ae638fc89efb Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Sun, 21 Feb 2021 15:18:10 +0200 Subject: [PATCH 05/34] fix image tracks build and clean up --- .../playkit/player/ExoPlayerWrapper.java | 28 +++---- .../playkit/player/TrackSelectionHelper.java | 79 ++++++++++++------- 2 files changed, 65 insertions(+), 42 deletions(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java index c0e892602..e0783f71f 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java @@ -29,6 +29,7 @@ import com.kaltura.android.exoplayer2.ExoPlayerLibraryInfo; import com.kaltura.android.exoplayer2.LoadControl; import com.kaltura.android.exoplayer2.MediaItem; +import com.kaltura.android.exoplayer2.ParserException; import com.kaltura.android.exoplayer2.PlaybackParameters; import com.kaltura.android.exoplayer2.Player; import com.kaltura.android.exoplayer2.SimpleExoPlayer; @@ -78,6 +79,7 @@ import com.kaltura.playkit.utils.NativeCookieJarBridge; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; @@ -879,31 +881,29 @@ public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray tra log.d("onTracksChanged"); String dashManifestString = ""; - + //if onTracksChanged happened when application went background, do not update the tracks. if (assertTrackSelectionIsNotNull("onTracksChanged()")) { - + //if the track info new -> map the available tracks. and when ready, notify user about available tracks. if (shouldGetTracksInfo) { CustomDashManifest customDashManifest = null; - if (player.getCurrentManifest() instanceof DashManifest) { + if (lastDataSink != null && player.getCurrentManifest() instanceof DashManifest) { byte[] bytes = lastDataSink.getData(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - dashManifestString = new String(bytes, StandardCharsets.UTF_8); - - try { - customDashManifest = new CustomDashManifestParser().parse(player.getMediaItemAt(0).playbackProperties.uri ,dashManifestString); - } catch (IOException e) { - e.printStackTrace(); - } + try { + dashManifestString = new String(bytes, "UTF-8"); + customDashManifest = new CustomDashManifestParser().parse(player.getMediaItemAt(0).playbackProperties.uri, dashManifestString); + } catch (IOException e) { + log.e("imageTracks assemble error " + e.getMessage()); + } finally { + lastDataSink = null; } } shouldGetTracksInfo = !trackSelectionHelper.prepareTracks(trackSelections, customDashManifest); } - - trackSelectionHelper.notifyAboutTrackChange(trackSelections); } + + trackSelectionHelper.notifyAboutTrackChange(trackSelections); } @Override diff --git a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java index f16469b90..69c146cc2 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java @@ -19,9 +19,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.kaltura.android.exoplayer2.C; import com.kaltura.android.exoplayer2.Format; import com.kaltura.android.exoplayer2.RendererCapabilities; +import com.kaltura.android.exoplayer2.dashmanifestparser.CustomAdaptationSet; import com.kaltura.android.exoplayer2.dashmanifestparser.CustomDashManifest; +import com.kaltura.android.exoplayer2.dashmanifestparser.CustomFormat; +import com.kaltura.android.exoplayer2.dashmanifestparser.CustomRepresentation; import com.kaltura.android.exoplayer2.source.TrackGroup; import com.kaltura.android.exoplayer2.source.TrackGroupArray; import com.kaltura.android.exoplayer2.trackselection.DefaultTrackSelector; @@ -180,7 +184,28 @@ boolean prepareTracks(TrackSelectionArray trackSelections, CustomDashManifest cu } warnAboutUnsupportedRendererTypes(); - PKTracks tracksInfo = buildTracks(); + + List rawImageTracks = new ArrayList<>(); + if (customDashManifest != null) { + for (int periodIndex = 0; periodIndex < customDashManifest.getPeriodCount(); periodIndex++) { + List adaptationSets = customDashManifest.getPeriod(periodIndex).adaptationSets; + + for (int adaptationSetIndex = 0 ; adaptationSetIndex < adaptationSets.size() ; adaptationSetIndex++) { + if (adaptationSets.get(adaptationSetIndex).type != C.TRACK_TYPE_IMAGE) { + continue; + } + List representations = adaptationSets.get(adaptationSetIndex).representations; + for (CustomRepresentation representation : representations) { + if (representation.format == null || representation.format.formatThumbnailInfo == null) { + continue; + } + rawImageTracks.add(representation.format); + } + } + } + } + + PKTracks tracksInfo = buildTracks(rawImageTracks); if (tracksInfoListener != null) { tracksInfoListener.onTracksInfoReady(tracksInfo); @@ -193,7 +218,7 @@ boolean prepareTracks(TrackSelectionArray trackSelections, CustomDashManifest cu * Actually build {@link PKTracks} object, based on the loaded manifest into Exoplayer. * This method knows how to filter unsupported/unknown formats, and create adaptive option when this is possible. */ - private PKTracks buildTracks() { + private PKTracks buildTracks(List rawImageTracks) { clearTracksLists(); @@ -293,29 +318,25 @@ private PKTracks buildTracks() { } } - if (mappedTrackInfo != null) { - TrackGroupArray imageTrackGroupArray = mappedTrackInfo.getUnmappedTrackGroups(); - for (int imageTrackIndex = 0 ; imageTrackIndex < imageTrackGroupArray.length; imageTrackIndex++) { - TrackGroup imageTrackGroup = imageTrackGroupArray.get(imageTrackIndex); - for (int trackIndex = 0; trackIndex < imageTrackGroup.length; trackIndex++) { - Format imageFormat = imageTrackGroup.getFormat(trackIndex); - Format.FormatThumbnailInfo formatThumbnailInfo = imageFormat.formatThumbnailInfo; - String uniqueId = getUniqueId(TRACK_TYPE_IMAGE, TRACK_TYPE_IMAGE, trackIndex); - imageTracks.add(trackIndex, new ImageTrack(uniqueId, - imageFormat.id, - imageFormat.bitrate, - imageFormat.width, - imageFormat.height, - formatThumbnailInfo.tilesHorizontal, - formatThumbnailInfo.tilesVertical, - formatThumbnailInfo.segmentDuration * Consts.MILLISECONDS_MULTIPLIER, - formatThumbnailInfo.startNumber, - formatThumbnailInfo.endNumber, - formatThumbnailInfo.presentationTimeOffset, - formatThumbnailInfo.timeScale, - formatThumbnailInfo.imageTemplateUrl - )); - } + if (rawImageTracks != null && !rawImageTracks.isEmpty()) { + for (int trackIndex = 0; trackIndex < rawImageTracks.size(); trackIndex++) { + CustomFormat imageFormat = rawImageTracks.get(trackIndex); + CustomFormat.FormatThumbnailInfo formatThumbnailInfo = imageFormat.formatThumbnailInfo; + String uniqueId = getUniqueId(TRACK_TYPE_IMAGE, TRACK_TYPE_IMAGE, trackIndex); + imageTracks.add(trackIndex, new ImageTrack(uniqueId, + imageFormat.id, + imageFormat.bitrate, + imageFormat.width, + imageFormat.height, + formatThumbnailInfo.tilesHorizontal, + formatThumbnailInfo.tilesVertical, + formatThumbnailInfo.segmentDuration * Consts.MILLISECONDS_MULTIPLIER, + formatThumbnailInfo.startNumber, + formatThumbnailInfo.endNumber, + formatThumbnailInfo.presentationTimeOffset, + formatThumbnailInfo.timeScale, + formatThumbnailInfo.imageTemplateUrl + )); } } @@ -538,7 +559,7 @@ private void maybeAddDisabledTextTrack() { if (textTracks.isEmpty()) { return; } - + String uniqueId = getUniqueId(TRACK_TYPE_TEXT, 0, TRACK_DISABLED); textTracks.add(0, new TextTrack(uniqueId, NONE, NONE, NONE, -1)); } @@ -588,7 +609,7 @@ private int getDefaultTrackIndex(List trackList, String las } else if (!isExternalSubtitle && pkSubtitlePreference == PKSubtitlePreference.INTERNAL) { defaultTrackIndex = i; break; - } + } } else { defaultTrackIndex = i; break; @@ -797,7 +818,7 @@ protected void changeTrack(String uniqueId) { tracksInfoListener.onImageTrackChanged(); return; } - + DefaultTrackSelector.ParametersBuilder parametersBuilder = selector.getParameters().buildUpon(); if (rendererIndex == TRACK_TYPE_TEXT) { //Disable text track renderer if needed. @@ -1373,6 +1394,7 @@ private void clearTracksLists() { videoTracks.clear(); audioTracks.clear(); textTracks.clear(); + imageTracks.clear(); for (Map.Entry> videoTrackEntry : videoTracksCodecsMap.entrySet()) { videoTrackEntry.getValue().clear(); } @@ -1524,6 +1546,7 @@ protected void stop() { videoTracks.clear(); audioTracks.clear(); textTracks.clear(); + imageTracks.clear(); } /** From 581c22d7fd8b268d357b77bc9beb5b104147bcca Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Sun, 21 Feb 2021 19:27:54 +0200 Subject: [PATCH 06/34] avoid exo parsing error add missing libs --- playkit/build.gradle | 17 + .../CustomDashManifestParser.java | 15 +- .../DashManifestParserForThumbnail.java | 1857 +++++++++++++++++ .../playkit/player/ExoPlayerWrapper.java | 3 + 4 files changed, 1886 insertions(+), 6 deletions(-) create mode 100644 playkit/src/main/java/com/kaltura/android/exoplayer2/source/dash/manifest/DashManifestParserForThumbnail.java diff --git a/playkit/build.gradle b/playkit/build.gradle index c8ac0c62c..4f3206ae6 100644 --- a/playkit/build.gradle +++ b/playkit/build.gradle @@ -44,6 +44,23 @@ dependencies { // Ok is (optionally) used by ExoPlayer now api 'com.squareup.okhttp3:okhttp:3.12.11' + def checkerframeworkVersion = '3.3.0' + def checkerframeworkCompatVersion = '2.5.0' + + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion + + def guavaVersion = '27.1-android' + + api ('com.google.guava:guava:' + guavaVersion) { + // Exclude dependencies that are only used by Guava at compile time + // (but declared as runtime deps) [internal b/168188131]. + exclude group: 'com.google.code.findbugs', module: 'jsr305' + exclude group: 'org.checkerframework', module: 'checker-compat-qual' + exclude group: 'com.google.errorprone', module: 'error_prone_annotations' + exclude group: 'com.google.j2objc', module: 'j2objc-annotations' + exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations' + } // Tests testImplementation 'junit:junit:4.12' testImplementation 'org.hamcrest:hamcrest-library:1.3' diff --git a/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomDashManifestParser.java b/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomDashManifestParser.java index 7375fe218..81837b207 100644 --- a/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomDashManifestParser.java +++ b/playkit/src/main/java/com/kaltura/android/exoplayer2/dashmanifestparser/CustomDashManifestParser.java @@ -7,6 +7,8 @@ import android.util.Pair; import android.util.Xml; import androidx.annotation.Nullable; + +import com.google.common.collect.ImmutableList; import com.kaltura.android.exoplayer2.C; import com.kaltura.android.exoplayer2.Format; import com.kaltura.android.exoplayer2.ParserException; @@ -40,6 +42,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.compatqual.NullableType; import org.xml.sax.helpers.DefaultHandler; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -322,7 +325,7 @@ protected Pair parsePeriod( parseSegmentTemplate( xpp, /* parent= */ null, - new ArrayList<>(), + ImmutableList.of(), periodStartUnixTimeMs, durationMs, baseUrlAvailabilityTimeOffsetUs, @@ -535,7 +538,7 @@ protected int parseContentType(XmlPullParser xpp) { * @return The scheme type and/or {@link SchemeData} parsed from the ContentProtection element. * Either or both may be null, depending on the ContentProtection element being parsed. */ - protected Pair parseContentProtection( + protected Pair<@NullableType String, @NullableType SchemeData> parseContentProtection( XmlPullParser xpp) throws XmlPullParserException, IOException { String schemeType = null; String licenseServerUrl = null; @@ -1821,14 +1824,14 @@ protected static int parseInt(XmlPullParser xpp, String name, int defaultValue) protected static long parseLong(XmlPullParser xpp, String name, long defaultValue) { String value = xpp.getAttributeValue(null, name); - boolean increase = false; + boolean isFloat = false; if (value !=null && value.contains(".")) { value = value.split("\\.")[0]; - increase = true; + isFloat = true; } long longValue = (value == null) ? defaultValue : Long.parseLong(value); - if (increase) { - return longValue + 1; + if (isFloat) { + return ++longValue; } return longValue; } diff --git a/playkit/src/main/java/com/kaltura/android/exoplayer2/source/dash/manifest/DashManifestParserForThumbnail.java b/playkit/src/main/java/com/kaltura/android/exoplayer2/source/dash/manifest/DashManifestParserForThumbnail.java new file mode 100644 index 000000000..452772a5d --- /dev/null +++ b/playkit/src/main/java/com/kaltura/android/exoplayer2/source/dash/manifest/DashManifestParserForThumbnail.java @@ -0,0 +1,1857 @@ +package com.kaltura.android.exoplayer2.source.dash.manifest; + +import android.net.Uri; +import android.text.TextUtils; +import android.util.Base64; +import android.util.Pair; +import android.util.Xml; +import androidx.annotation.Nullable; +import com.kaltura.android.exoplayer2.C; +import com.kaltura.android.exoplayer2.Format; +import com.kaltura.android.exoplayer2.ParserException; +import com.kaltura.android.exoplayer2.drm.DrmInitData; +import com.kaltura.android.exoplayer2.drm.DrmInitData.SchemeData; +import com.kaltura.android.exoplayer2.extractor.mp4.PsshAtomUtil; +import com.kaltura.android.exoplayer2.metadata.emsg.EventMessage; +import com.kaltura.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentList; +import com.kaltura.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTemplate; +import com.kaltura.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTimelineElement; +import com.kaltura.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase; +import com.kaltura.android.exoplayer2.upstream.ParsingLoadable; +import com.kaltura.android.exoplayer2.util.Assertions; +import com.kaltura.android.exoplayer2.util.Log; +import com.kaltura.android.exoplayer2.util.MimeTypes; +import com.kaltura.android.exoplayer2.util.UriUtil; +import com.kaltura.android.exoplayer2.util.Util; +import com.kaltura.android.exoplayer2.util.XmlPullParserUtil; +import com.google.common.base.Charsets; +import com.google.common.collect.ImmutableList; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.xml.sax.helpers.DefaultHandler; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; +import org.xmlpull.v1.XmlSerializer; + +/** + * A parser of media presentation description files. + */ +public class DashManifestParserForThumbnail extends DefaultHandler + implements ParsingLoadable.Parser { + + private static final String TAG = "MpdParser"; + + private static final Pattern FRAME_RATE_PATTERN = Pattern.compile("(\\d+)(?:/(\\d+))?"); + + private static final Pattern CEA_608_ACCESSIBILITY_PATTERN = Pattern.compile("CC([1-4])=.*"); + private static final Pattern CEA_708_ACCESSIBILITY_PATTERN = + Pattern.compile("([1-9]|[1-5][0-9]|6[0-3])=.*"); + + /** + * Maps the value attribute of an AudioElementConfiguration with schemeIdUri + * "urn:mpeg:mpegB:cicp:ChannelConfiguration", as defined by ISO 23001-8 clause 8.1, to a channel + * count. + */ + private static final int[] MPEG_CHANNEL_CONFIGURATION_MAPPING = + new int[] { + Format.NO_VALUE, 1, 2, 3, 4, 5, 6, 8, 2, 3, 4, 7, 8, 24, 8, 12, 10, 12, 14, 12, 14 + }; + + private final XmlPullParserFactory xmlParserFactory; + + public DashManifestParserForThumbnail() { + try { + xmlParserFactory = XmlPullParserFactory.newInstance(); + } catch (XmlPullParserException e) { + throw new RuntimeException("Couldn't create XmlPullParserFactory instance", e); + } + } + + // MPD parsing. + + @Override + public DashManifest parse(Uri uri, InputStream inputStream) throws IOException { + try { + XmlPullParser xpp = xmlParserFactory.newPullParser(); + xpp.setInput(inputStream, null); + int eventType = xpp.next(); + if (eventType != XmlPullParser.START_TAG || !"MPD".equals(xpp.getName())) { + throw new ParserException( + "inputStream does not contain a valid media presentation description"); + } + return parseMediaPresentationDescription(xpp, uri.toString()); + } catch (XmlPullParserException e) { + throw new ParserException(e); + } + } + + protected DashManifest parseMediaPresentationDescription(XmlPullParser xpp, + String baseUrl) throws XmlPullParserException, IOException { + long availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", C.TIME_UNSET); + long durationMs = parseDuration(xpp, "mediaPresentationDuration", C.TIME_UNSET); + long minBufferTimeMs = parseDuration(xpp, "minBufferTime", C.TIME_UNSET); + String typeString = xpp.getAttributeValue(null, "type"); + boolean dynamic = "dynamic".equals(typeString); + long minUpdateTimeMs = dynamic ? parseDuration(xpp, "minimumUpdatePeriod", C.TIME_UNSET) + : C.TIME_UNSET; + long timeShiftBufferDepthMs = dynamic + ? parseDuration(xpp, "timeShiftBufferDepth", C.TIME_UNSET) : C.TIME_UNSET; + long suggestedPresentationDelayMs = dynamic + ? parseDuration(xpp, "suggestedPresentationDelay", C.TIME_UNSET) : C.TIME_UNSET; + long publishTimeMs = parseDateTime(xpp, "publishTime", C.TIME_UNSET); + ProgramInformation programInformation = null; + UtcTimingElement utcTiming = null; + Uri location = null; + ServiceDescriptionElement serviceDescription = null; + long baseUrlAvailabilityTimeOffsetUs = dynamic ? 0 : C.TIME_UNSET; + + List periods = new ArrayList<>(); + long nextPeriodStartMs = dynamic ? C.TIME_UNSET : 0; + boolean seenEarlyAccessPeriod = false; + boolean seenFirstBaseUrl = false; + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "BaseURL")) { + if (!seenFirstBaseUrl) { + baseUrlAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, baseUrlAvailabilityTimeOffsetUs); + baseUrl = parseBaseUrl(xpp, baseUrl); + seenFirstBaseUrl = true; + } + } else if (XmlPullParserUtil.isStartTag(xpp, "ProgramInformation")) { + programInformation = parseProgramInformation(xpp); + } else if (XmlPullParserUtil.isStartTag(xpp, "UTCTiming")) { + utcTiming = parseUtcTiming(xpp); + } else if (XmlPullParserUtil.isStartTag(xpp, "Location")) { + location = Uri.parse(xpp.nextText()); + } else if (XmlPullParserUtil.isStartTag(xpp, "ServiceDescription")) { + serviceDescription = parseServiceDescription(xpp); + } else if (XmlPullParserUtil.isStartTag(xpp, "Period") && !seenEarlyAccessPeriod) { + Pair periodWithDurationMs = + parsePeriod( + xpp, + baseUrl, + nextPeriodStartMs, + baseUrlAvailabilityTimeOffsetUs, + availabilityStartTime, + timeShiftBufferDepthMs); + Period period = periodWithDurationMs.first; + if (period.startMs == C.TIME_UNSET) { + if (dynamic) { + // This is an early access period. Ignore it. All subsequent periods must also be + // early access. + seenEarlyAccessPeriod = true; + } else { + throw new ParserException("Unable to determine start of period " + periods.size()); + } + } else { + long periodDurationMs = periodWithDurationMs.second; + nextPeriodStartMs = periodDurationMs == C.TIME_UNSET ? C.TIME_UNSET + : (period.startMs + periodDurationMs); + periods.add(period); + } + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "MPD")); + + if (durationMs == C.TIME_UNSET) { + if (nextPeriodStartMs != C.TIME_UNSET) { + // If we know the end time of the final period, we can use it as the duration. + durationMs = nextPeriodStartMs; + } else if (!dynamic) { + throw new ParserException("Unable to determine duration of static manifest."); + } + } + + if (periods.isEmpty()) { + throw new ParserException("No periods found."); + } + + return buildMediaPresentationDescription( + availabilityStartTime, + durationMs, + minBufferTimeMs, + dynamic, + minUpdateTimeMs, + timeShiftBufferDepthMs, + suggestedPresentationDelayMs, + publishTimeMs, + programInformation, + utcTiming, + serviceDescription, + location, + periods); + } + + protected DashManifest buildMediaPresentationDescription( + long availabilityStartTime, + long durationMs, + long minBufferTimeMs, + boolean dynamic, + long minUpdateTimeMs, + long timeShiftBufferDepthMs, + long suggestedPresentationDelayMs, + long publishTimeMs, + @Nullable ProgramInformation programInformation, + @Nullable UtcTimingElement utcTiming, + @Nullable ServiceDescriptionElement serviceDescription, + @Nullable Uri location, + List periods) { + return new DashManifest( + availabilityStartTime, + durationMs, + minBufferTimeMs, + dynamic, + minUpdateTimeMs, + timeShiftBufferDepthMs, + suggestedPresentationDelayMs, + publishTimeMs, + programInformation, + utcTiming, + serviceDescription, + location, + periods); + } + + protected UtcTimingElement parseUtcTiming(XmlPullParser xpp) { + String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); + String value = xpp.getAttributeValue(null, "value"); + return buildUtcTimingElement(schemeIdUri, value); + } + + protected UtcTimingElement buildUtcTimingElement(String schemeIdUri, String value) { + return new UtcTimingElement(schemeIdUri, value); + } + + protected ServiceDescriptionElement parseServiceDescription(XmlPullParser xpp) + throws XmlPullParserException, IOException { + long targetOffsetMs = C.TIME_UNSET; + long minOffsetMs = C.TIME_UNSET; + long maxOffsetMs = C.TIME_UNSET; + float minPlaybackSpeed = C.RATE_UNSET; + float maxPlaybackSpeed = C.RATE_UNSET; + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "Latency")) { + targetOffsetMs = parseLong(xpp, "target", C.TIME_UNSET); + minOffsetMs = parseLong(xpp, "min", C.TIME_UNSET); + maxOffsetMs = parseLong(xpp, "max", C.TIME_UNSET); + } else if (XmlPullParserUtil.isStartTag(xpp, "PlaybackRate")) { + minPlaybackSpeed = parseFloat(xpp, "min", C.RATE_UNSET); + maxPlaybackSpeed = parseFloat(xpp, "max", C.RATE_UNSET); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "ServiceDescription")); + return new ServiceDescriptionElement( + targetOffsetMs, minOffsetMs, maxOffsetMs, minPlaybackSpeed, maxPlaybackSpeed); + } + + protected Pair parsePeriod( + XmlPullParser xpp, + String baseUrl, + long defaultStartMs, + long baseUrlAvailabilityTimeOffsetUs, + long availabilityStartTimeMs, + long timeShiftBufferDepthMs) + throws XmlPullParserException, IOException { + @Nullable String id = xpp.getAttributeValue(null, "id"); + long startMs = parseDuration(xpp, "start", defaultStartMs); + long periodStartUnixTimeMs = + availabilityStartTimeMs != C.TIME_UNSET ? availabilityStartTimeMs + startMs : C.TIME_UNSET; + long durationMs = parseDuration(xpp, "duration", C.TIME_UNSET); + @Nullable SegmentBase segmentBase = null; + @Nullable Descriptor assetIdentifier = null; + List adaptationSets = new ArrayList<>(); + List eventStreams = new ArrayList<>(); + boolean seenFirstBaseUrl = false; + long segmentBaseAvailabilityTimeOffsetUs = C.TIME_UNSET; + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "BaseURL")) { + if (!seenFirstBaseUrl) { + baseUrlAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, baseUrlAvailabilityTimeOffsetUs); + baseUrl = parseBaseUrl(xpp, baseUrl); + seenFirstBaseUrl = true; + } + } else if (XmlPullParserUtil.isStartTag(xpp, "AdaptationSet")) { + adaptationSets.add( + parseAdaptationSet( + xpp, + baseUrl, + segmentBase, + durationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs, + periodStartUnixTimeMs, + timeShiftBufferDepthMs)); + } else if (XmlPullParserUtil.isStartTag(xpp, "EventStream")) { + eventStreams.add(parseEventStream(xpp)); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { + segmentBase = parseSegmentBase(xpp, /* parent= */ null); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { + segmentBaseAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, /* parentAvailabilityTimeOffsetUs= */ C.TIME_UNSET); + segmentBase = + parseSegmentList( + xpp, + /* parent= */ null, + periodStartUnixTimeMs, + durationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs, + timeShiftBufferDepthMs); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { + segmentBaseAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, /* parentAvailabilityTimeOffsetUs= */ C.TIME_UNSET); + segmentBase = + parseSegmentTemplate( + xpp, + /* parent= */ null, + ImmutableList.of(), + periodStartUnixTimeMs, + durationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs, + timeShiftBufferDepthMs); + } else if (XmlPullParserUtil.isStartTag(xpp, "AssetIdentifier")) { + assetIdentifier = parseDescriptor(xpp, "AssetIdentifier"); + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "Period")); + + return Pair.create( + buildPeriod(id, startMs, adaptationSets, eventStreams, assetIdentifier), durationMs); + } + + protected Period buildPeriod( + @Nullable String id, + long startMs, + List adaptationSets, + List eventStreams, + @Nullable Descriptor assetIdentifier) { + return new Period(id, startMs, adaptationSets, eventStreams, assetIdentifier); + } + + // AdaptationSet parsing. + + protected AdaptationSet parseAdaptationSet( + XmlPullParser xpp, + String baseUrl, + @Nullable SegmentBase segmentBase, + long periodDurationMs, + long baseUrlAvailabilityTimeOffsetUs, + long segmentBaseAvailabilityTimeOffsetUs, + long periodStartUnixTimeMs, + long timeShiftBufferDepthMs) + throws XmlPullParserException, IOException { + int id = parseInt(xpp, "id", AdaptationSet.ID_UNSET); + int contentType = parseContentType(xpp); + + String mimeType = xpp.getAttributeValue(null, "mimeType"); + String codecs = xpp.getAttributeValue(null, "codecs"); + int width = parseInt(xpp, "width", Format.NO_VALUE); + int height = parseInt(xpp, "height", Format.NO_VALUE); + float frameRate = parseFrameRate(xpp, Format.NO_VALUE); + int audioChannels = Format.NO_VALUE; + int audioSamplingRate = parseInt(xpp, "audioSamplingRate", Format.NO_VALUE); + String language = xpp.getAttributeValue(null, "lang"); + String label = xpp.getAttributeValue(null, "label"); + String drmSchemeType = null; + ArrayList drmSchemeDatas = new ArrayList<>(); + ArrayList inbandEventStreams = new ArrayList<>(); + ArrayList accessibilityDescriptors = new ArrayList<>(); + ArrayList roleDescriptors = new ArrayList<>(); + ArrayList essentialProperties = new ArrayList<>(); + ArrayList supplementalProperties = new ArrayList<>(); + List representationInfos = new ArrayList<>(); + + boolean seenFirstBaseUrl = false; + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "BaseURL")) { + if (!seenFirstBaseUrl) { + baseUrlAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, baseUrlAvailabilityTimeOffsetUs); + baseUrl = parseBaseUrl(xpp, baseUrl); + seenFirstBaseUrl = true; + } + } else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) { + Pair contentProtection = parseContentProtection(xpp); + if (contentProtection.first != null) { + drmSchemeType = contentProtection.first; + } + if (contentProtection.second != null) { + drmSchemeDatas.add(contentProtection.second); + } + } else if (XmlPullParserUtil.isStartTag(xpp, "ContentComponent")) { + language = checkLanguageConsistency(language, xpp.getAttributeValue(null, "lang")); + contentType = checkContentTypeConsistency(contentType, parseContentType(xpp)); + } else if (XmlPullParserUtil.isStartTag(xpp, "Role")) { + roleDescriptors.add(parseDescriptor(xpp, "Role")); + } else if (XmlPullParserUtil.isStartTag(xpp, "AudioChannelConfiguration")) { + audioChannels = parseAudioChannelConfiguration(xpp); + } else if (XmlPullParserUtil.isStartTag(xpp, "Accessibility")) { + accessibilityDescriptors.add(parseDescriptor(xpp, "Accessibility")); + } else if (XmlPullParserUtil.isStartTag(xpp, "EssentialProperty")) { + essentialProperties.add(parseDescriptor(xpp, "EssentialProperty")); + } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) { + supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty")); + } else if (XmlPullParserUtil.isStartTag(xpp, "Representation")) { + RepresentationInfo representationInfo = + parseRepresentation( + xpp, + baseUrl, + mimeType, + codecs, + width, + height, + frameRate, + audioChannels, + audioSamplingRate, + language, + roleDescriptors, + accessibilityDescriptors, + essentialProperties, + supplementalProperties, + segmentBase, + periodStartUnixTimeMs, + periodDurationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs, + timeShiftBufferDepthMs); + contentType = + checkContentTypeConsistency( + contentType, MimeTypes.getTrackType(representationInfo.format.sampleMimeType)); + representationInfos.add(representationInfo); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { + segmentBase = parseSegmentBase(xpp, (SingleSegmentBase) segmentBase); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { + segmentBaseAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, segmentBaseAvailabilityTimeOffsetUs); + segmentBase = + parseSegmentList( + xpp, + (SegmentList) segmentBase, + periodStartUnixTimeMs, + periodDurationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs, + timeShiftBufferDepthMs); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { + segmentBaseAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, segmentBaseAvailabilityTimeOffsetUs); + segmentBase = + parseSegmentTemplate( + xpp, + (SegmentTemplate) segmentBase, + supplementalProperties, + periodStartUnixTimeMs, + periodDurationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs, + timeShiftBufferDepthMs); + } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { + inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); + } else if (XmlPullParserUtil.isStartTag(xpp, "Label")) { + label = parseLabel(xpp); + } else if (XmlPullParserUtil.isStartTag(xpp)) { + parseAdaptationSetChild(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "AdaptationSet")); + + // Build the representations. + List representations = new ArrayList<>(representationInfos.size()); + for (int i = 0; i < representationInfos.size(); i++) { + representations.add( + buildRepresentation( + representationInfos.get(i), + label, + drmSchemeType, + drmSchemeDatas, + inbandEventStreams)); + } + + return buildAdaptationSet( + id, + contentType, + representations, + accessibilityDescriptors, + essentialProperties, + supplementalProperties); + } + + protected AdaptationSet buildAdaptationSet( + int id, + int contentType, + List representations, + List accessibilityDescriptors, + List essentialProperties, + List supplementalProperties) { + return new AdaptationSet( + id, + contentType, + representations, + accessibilityDescriptors, + essentialProperties, + supplementalProperties); + } + + protected int parseContentType(XmlPullParser xpp) { + String contentType = xpp.getAttributeValue(null, "contentType"); + return TextUtils.isEmpty(contentType) ? C.TRACK_TYPE_UNKNOWN + : MimeTypes.BASE_TYPE_AUDIO.equals(contentType) ? C.TRACK_TYPE_AUDIO + : MimeTypes.BASE_TYPE_VIDEO.equals(contentType) ? C.TRACK_TYPE_VIDEO + : MimeTypes.BASE_TYPE_TEXT.equals(contentType) ? C.TRACK_TYPE_TEXT + : C.TRACK_TYPE_UNKNOWN; + } + + /** + * Parses a ContentProtection element. + * + * @param xpp The parser from which to read. + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + * @return The scheme type and/or {@link SchemeData} parsed from the ContentProtection element. + * Either or both may be null, depending on the ContentProtection element being parsed. + */ + protected Pair<@NullableType String, @NullableType SchemeData> parseContentProtection( + XmlPullParser xpp) throws XmlPullParserException, IOException { + String schemeType = null; + String licenseServerUrl = null; + byte[] data = null; + UUID uuid = null; + + String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); + if (schemeIdUri != null) { + switch (Util.toLowerInvariant(schemeIdUri)) { + case "urn:mpeg:dash:mp4protection:2011": + schemeType = xpp.getAttributeValue(null, "value"); + String defaultKid = XmlPullParserUtil.getAttributeValueIgnorePrefix(xpp, "default_KID"); + if (!TextUtils.isEmpty(defaultKid) + && !"00000000-0000-0000-0000-000000000000".equals(defaultKid)) { + String[] defaultKidStrings = defaultKid.split("\\s+"); + UUID[] defaultKids = new UUID[defaultKidStrings.length]; + for (int i = 0; i < defaultKidStrings.length; i++) { + defaultKids[i] = UUID.fromString(defaultKidStrings[i]); + } + data = PsshAtomUtil.buildPsshAtom(C.COMMON_PSSH_UUID, defaultKids, null); + uuid = C.COMMON_PSSH_UUID; + } + break; + case "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95": + uuid = C.PLAYREADY_UUID; + break; + case "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed": + uuid = C.WIDEVINE_UUID; + break; + default: + break; + } + } + + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "ms:laurl")) { + licenseServerUrl = xpp.getAttributeValue(null, "licenseUrl"); + } else if (data == null + && XmlPullParserUtil.isStartTagIgnorePrefix(xpp, "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"); + data = null; + } + } else if (data == null + && C.PLAYREADY_UUID.equals(uuid) + && XmlPullParserUtil.isStartTag(xpp, "mspr:pro") + && xpp.next() == XmlPullParser.TEXT) { + // The mspr:pro element is defined in DASH Content Protection using Microsoft PlayReady. + data = + PsshAtomUtil.buildPsshAtom( + C.PLAYREADY_UUID, Base64.decode(xpp.getText(), Base64.DEFAULT)); + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection")); + SchemeData schemeData = + uuid != null ? new SchemeData(uuid, licenseServerUrl, MimeTypes.VIDEO_MP4, data) : null; + return Pair.create(schemeType, schemeData); + } + + /** + * Parses children of AdaptationSet elements not specifically parsed elsewhere. + * + * @param xpp The XmpPullParser from which the AdaptationSet child should be parsed. + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + */ + protected void parseAdaptationSetChild(XmlPullParser xpp) + throws XmlPullParserException, IOException { + maybeSkipTag(xpp); + } + + // Representation parsing. + + protected RepresentationInfo parseRepresentation( + XmlPullParser xpp, + String baseUrl, + @Nullable String adaptationSetMimeType, + @Nullable String adaptationSetCodecs, + int adaptationSetWidth, + int adaptationSetHeight, + float adaptationSetFrameRate, + int adaptationSetAudioChannels, + int adaptationSetAudioSamplingRate, + @Nullable String adaptationSetLanguage, + List adaptationSetRoleDescriptors, + List adaptationSetAccessibilityDescriptors, + List adaptationSetEssentialProperties, + List adaptationSetSupplementalProperties, + @Nullable SegmentBase segmentBase, + long periodStartUnixTimeMs, + long periodDurationMs, + long baseUrlAvailabilityTimeOffsetUs, + long segmentBaseAvailabilityTimeOffsetUs, + long timeShiftBufferDepthMs) + throws XmlPullParserException, IOException { + String id = xpp.getAttributeValue(null, "id"); + int bandwidth = parseInt(xpp, "bandwidth", Format.NO_VALUE); + + String mimeType = parseString(xpp, "mimeType", adaptationSetMimeType); + String codecs = parseString(xpp, "codecs", adaptationSetCodecs); + int width = parseInt(xpp, "width", adaptationSetWidth); + int height = parseInt(xpp, "height", adaptationSetHeight); + float frameRate = parseFrameRate(xpp, adaptationSetFrameRate); + int audioChannels = adaptationSetAudioChannels; + int audioSamplingRate = parseInt(xpp, "audioSamplingRate", adaptationSetAudioSamplingRate); + String drmSchemeType = null; + ArrayList drmSchemeDatas = new ArrayList<>(); + ArrayList inbandEventStreams = new ArrayList<>(); + ArrayList essentialProperties = new ArrayList<>(adaptationSetEssentialProperties); + ArrayList supplementalProperties = + new ArrayList<>(adaptationSetSupplementalProperties); + + boolean seenFirstBaseUrl = false; + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "BaseURL")) { + if (!seenFirstBaseUrl) { + baseUrlAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, baseUrlAvailabilityTimeOffsetUs); + baseUrl = parseBaseUrl(xpp, baseUrl); + seenFirstBaseUrl = true; + } + } else if (XmlPullParserUtil.isStartTag(xpp, "AudioChannelConfiguration")) { + audioChannels = parseAudioChannelConfiguration(xpp); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { + segmentBase = parseSegmentBase(xpp, (SingleSegmentBase) segmentBase); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { + segmentBaseAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, segmentBaseAvailabilityTimeOffsetUs); + segmentBase = + parseSegmentList( + xpp, + (SegmentList) segmentBase, + periodStartUnixTimeMs, + periodDurationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs, + timeShiftBufferDepthMs); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { + segmentBaseAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, segmentBaseAvailabilityTimeOffsetUs); + segmentBase = + parseSegmentTemplate( + xpp, + (SegmentTemplate) segmentBase, + adaptationSetSupplementalProperties, + periodStartUnixTimeMs, + periodDurationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs, + timeShiftBufferDepthMs); + } else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) { + Pair contentProtection = parseContentProtection(xpp); + if (contentProtection.first != null) { + drmSchemeType = contentProtection.first; + } + if (contentProtection.second != null) { + drmSchemeDatas.add(contentProtection.second); + } + } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { + inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); + } else if (XmlPullParserUtil.isStartTag(xpp, "EssentialProperty")) { + essentialProperties.add(parseDescriptor(xpp, "EssentialProperty")); + } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) { + supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty")); + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "Representation")); + + Format format = + buildFormat( + id, + mimeType, + width, + height, + frameRate, + audioChannels, + audioSamplingRate, + bandwidth, + adaptationSetLanguage, + adaptationSetRoleDescriptors, + adaptationSetAccessibilityDescriptors, + codecs, + essentialProperties, + supplementalProperties); + segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase(); + + return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeType, drmSchemeDatas, + inbandEventStreams, Representation.REVISION_ID_DEFAULT); + } + + protected Format buildFormat( + @Nullable String id, + @Nullable String containerMimeType, + int width, + int height, + float frameRate, + int audioChannels, + int audioSamplingRate, + int bitrate, + @Nullable String language, + List roleDescriptors, + List accessibilityDescriptors, + @Nullable String codecs, + List essentialProperties, + List supplementalProperties) { + @Nullable String sampleMimeType = getSampleMimeType(containerMimeType, codecs); + if (MimeTypes.AUDIO_E_AC3.equals(sampleMimeType)) { + sampleMimeType = parseEac3SupplementalProperties(supplementalProperties); + } + @C.SelectionFlags int selectionFlags = parseSelectionFlagsFromRoleDescriptors(roleDescriptors); + @C.RoleFlags int roleFlags = parseRoleFlagsFromRoleDescriptors(roleDescriptors); + roleFlags |= parseRoleFlagsFromAccessibilityDescriptors(accessibilityDescriptors); + roleFlags |= parseRoleFlagsFromProperties(essentialProperties); + roleFlags |= parseRoleFlagsFromProperties(supplementalProperties); + + Format.Builder formatBuilder = + new Format.Builder() + .setId(id) + .setContainerMimeType(containerMimeType) + .setSampleMimeType(sampleMimeType) + .setCodecs(codecs) + .setPeakBitrate(bitrate) + .setSelectionFlags(selectionFlags) + .setRoleFlags(roleFlags) + .setLanguage(language); + + if (MimeTypes.isVideo(sampleMimeType)) { + formatBuilder.setWidth(width).setHeight(height).setFrameRate(frameRate); + } else if (MimeTypes.isAudio(sampleMimeType)) { + formatBuilder.setChannelCount(audioChannels).setSampleRate(audioSamplingRate); + } else if (MimeTypes.isText(sampleMimeType)) { + int accessibilityChannel = Format.NO_VALUE; + if (MimeTypes.APPLICATION_CEA608.equals(sampleMimeType)) { + accessibilityChannel = parseCea608AccessibilityChannel(accessibilityDescriptors); + } else if (MimeTypes.APPLICATION_CEA708.equals(sampleMimeType)) { + accessibilityChannel = parseCea708AccessibilityChannel(accessibilityDescriptors); + } + formatBuilder.setAccessibilityChannel(accessibilityChannel); + } + + return formatBuilder.build(); + } + + protected Representation buildRepresentation( + RepresentationInfo representationInfo, + @Nullable String label, + @Nullable String extraDrmSchemeType, + ArrayList extraDrmSchemeDatas, + ArrayList extraInbandEventStreams) { + Format.Builder formatBuilder = representationInfo.format.buildUpon(); + if (label != null) { + formatBuilder.setLabel(label); + } + @Nullable String drmSchemeType = representationInfo.drmSchemeType; + if (drmSchemeType == null) { + drmSchemeType = extraDrmSchemeType; + } + ArrayList drmSchemeDatas = representationInfo.drmSchemeDatas; + drmSchemeDatas.addAll(extraDrmSchemeDatas); + if (!drmSchemeDatas.isEmpty()) { + filterRedundantIncompleteSchemeDatas(drmSchemeDatas); + formatBuilder.setDrmInitData(new DrmInitData(drmSchemeType, drmSchemeDatas)); + } + ArrayList inbandEventStreams = representationInfo.inbandEventStreams; + inbandEventStreams.addAll(extraInbandEventStreams); + return Representation.newInstance( + representationInfo.revisionId, + formatBuilder.build(), + representationInfo.baseUrl, + representationInfo.segmentBase, + inbandEventStreams); + } + + // SegmentBase, SegmentList and SegmentTemplate parsing. + + protected SingleSegmentBase parseSegmentBase( + XmlPullParser xpp, @Nullable SingleSegmentBase parent) + throws XmlPullParserException, IOException { + + long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); + long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", + parent != null ? parent.presentationTimeOffset : 0); + + long indexStart = parent != null ? parent.indexStart : 0; + long indexLength = parent != null ? parent.indexLength : 0; + String indexRangeText = xpp.getAttributeValue(null, "indexRange"); + if (indexRangeText != null) { + String[] indexRange = indexRangeText.split("-"); + indexStart = Long.parseLong(indexRange[0]); + indexLength = Long.parseLong(indexRange[1]) - indexStart + 1; + } + + @Nullable RangedUri initialization = parent != null ? parent.initialization : null; + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "Initialization")) { + initialization = parseInitialization(xpp); + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "SegmentBase")); + + return buildSingleSegmentBase(initialization, timescale, presentationTimeOffset, indexStart, + indexLength); + } + + protected SingleSegmentBase buildSingleSegmentBase(RangedUri initialization, long timescale, + long presentationTimeOffset, long indexStart, long indexLength) { + return new SingleSegmentBase(initialization, timescale, presentationTimeOffset, indexStart, + indexLength); + } + + protected SegmentList parseSegmentList( + XmlPullParser xpp, + @Nullable SegmentList parent, + long periodStartUnixTimeMs, + long periodDurationMs, + long baseUrlAvailabilityTimeOffsetUs, + long segmentBaseAvailabilityTimeOffsetUs, + long timeShiftBufferDepthMs) + throws XmlPullParserException, IOException { + + long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); + long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", + parent != null ? parent.presentationTimeOffset : 0); + long duration = parseLong(xpp, "duration", parent != null ? parent.duration : C.TIME_UNSET); + long startNumber = parseLong(xpp, "startNumber", parent != null ? parent.startNumber : 1); + long availabilityTimeOffsetUs = + getFinalAvailabilityTimeOffset( + baseUrlAvailabilityTimeOffsetUs, segmentBaseAvailabilityTimeOffsetUs); + + RangedUri initialization = null; + List timeline = null; + List segments = null; + + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "Initialization")) { + initialization = parseInitialization(xpp); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTimeline")) { + timeline = parseSegmentTimeline(xpp, timescale, periodDurationMs); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentURL")) { + if (segments == null) { + segments = new ArrayList<>(); + } + segments.add(parseSegmentUrl(xpp)); + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "SegmentList")); + + if (parent != null) { + initialization = initialization != null ? initialization : parent.initialization; + timeline = timeline != null ? timeline : parent.segmentTimeline; + segments = segments != null ? segments : parent.mediaSegments; + } + + return buildSegmentList( + initialization, + timescale, + presentationTimeOffset, + startNumber, + duration, + timeline, + availabilityTimeOffsetUs, + segments, + timeShiftBufferDepthMs, + periodStartUnixTimeMs); + } + + protected SegmentList buildSegmentList( + RangedUri initialization, + long timescale, + long presentationTimeOffset, + long startNumber, + long duration, + @Nullable List timeline, + long availabilityTimeOffsetUs, + @Nullable List segments, + long timeShiftBufferDepthMs, + long periodStartUnixTimeMs) { + return new SegmentList( + initialization, + timescale, + presentationTimeOffset, + startNumber, + duration, + timeline, + availabilityTimeOffsetUs, + segments, + C.msToUs(timeShiftBufferDepthMs), + C.msToUs(periodStartUnixTimeMs)); + } + + protected SegmentTemplate parseSegmentTemplate( + XmlPullParser xpp, + @Nullable SegmentTemplate parent, + List adaptationSetSupplementalProperties, + long periodStartUnixTimeMs, + long periodDurationMs, + long baseUrlAvailabilityTimeOffsetUs, + long segmentBaseAvailabilityTimeOffsetUs, + long timeShiftBufferDepthMs) + throws XmlPullParserException, IOException { + long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); + long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", + parent != null ? parent.presentationTimeOffset : 0); + long duration = parseLong(xpp, "duration", parent != null ? parent.duration : C.TIME_UNSET); + long startNumber = parseLong(xpp, "startNumber", parent != null ? parent.startNumber : 1); + long endNumber = + parseLastSegmentNumberSupplementalProperty(adaptationSetSupplementalProperties); + if (endNumber == -1) { + endNumber = parseLong(xpp, "endNumber", parent != null ? parent.endNumber : -1); + } + + long availabilityTimeOffsetUs = + getFinalAvailabilityTimeOffset( + baseUrlAvailabilityTimeOffsetUs, segmentBaseAvailabilityTimeOffsetUs); + + UrlTemplate mediaTemplate = parseUrlTemplate(xpp, "media", + parent != null ? parent.mediaTemplate : null); + UrlTemplate initializationTemplate = parseUrlTemplate(xpp, "initialization", + parent != null ? parent.initializationTemplate : null); + + RangedUri initialization = null; + List timeline = null; + + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "Initialization")) { + initialization = parseInitialization(xpp); + } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTimeline")) { + timeline = parseSegmentTimeline(xpp, timescale, periodDurationMs); + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "SegmentTemplate")); + + if (parent != null) { + initialization = initialization != null ? initialization : parent.initialization; + timeline = timeline != null ? timeline : parent.segmentTimeline; + } + + return buildSegmentTemplate( + initialization, + timescale, + presentationTimeOffset, + startNumber, + endNumber, + duration, + timeline, + availabilityTimeOffsetUs, + initializationTemplate, + mediaTemplate, + timeShiftBufferDepthMs, + periodStartUnixTimeMs); + } + + protected SegmentTemplate buildSegmentTemplate( + RangedUri initialization, + long timescale, + long presentationTimeOffset, + long startNumber, + long endNumber, + long duration, + List timeline, + long availabilityTimeOffsetUs, + @Nullable UrlTemplate initializationTemplate, + @Nullable UrlTemplate mediaTemplate, + long timeShiftBufferDepthMs, + long periodStartUnixTimeMs) { + return new SegmentTemplate( + initialization, + timescale, + presentationTimeOffset, + startNumber, + endNumber, + duration, + timeline, + availabilityTimeOffsetUs, + initializationTemplate, + mediaTemplate, + C.msToUs(timeShiftBufferDepthMs), + C.msToUs(periodStartUnixTimeMs)); + } + + /** + * Parses a single EventStream node in the manifest. + * + * @param xpp The current xml parser. + * @return The {@link EventStream} parsed from this EventStream node. + * @throws XmlPullParserException If there is any error parsing this node. + * @throws IOException If there is any error reading from the underlying input stream. + */ + protected EventStream parseEventStream(XmlPullParser xpp) + throws XmlPullParserException, IOException { + String schemeIdUri = parseString(xpp, "schemeIdUri", ""); + String value = parseString(xpp, "value", ""); + long timescale = parseLong(xpp, "timescale", 1); + List> eventMessages = new ArrayList<>(); + ByteArrayOutputStream scratchOutputStream = new ByteArrayOutputStream(512); + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "Event")) { + Pair event = + parseEvent(xpp, schemeIdUri, value, timescale, scratchOutputStream); + eventMessages.add(event); + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "EventStream")); + + long[] presentationTimesUs = new long[eventMessages.size()]; + EventMessage[] events = new EventMessage[eventMessages.size()]; + for (int i = 0; i < eventMessages.size(); i++) { + Pair event = eventMessages.get(i); + presentationTimesUs[i] = event.first; + events[i] = event.second; + } + return buildEventStream(schemeIdUri, value, timescale, presentationTimesUs, events); + } + + protected EventStream buildEventStream(String schemeIdUri, String value, long timescale, + long[] presentationTimesUs, EventMessage[] events) { + return new EventStream(schemeIdUri, value, timescale, presentationTimesUs, events); + } + + /** + * Parses a single Event node in the manifest. + * + * @param xpp The current xml parser. + * @param schemeIdUri The schemeIdUri of the parent EventStream. + * @param value The schemeIdUri of the parent EventStream. + * @param timescale The timescale of the parent EventStream. + * @param scratchOutputStream A {@link ByteArrayOutputStream} that is used when parsing event + * objects. + * @return A pair containing the node's presentation timestamp in microseconds and the parsed + * {@link EventMessage}. + * @throws XmlPullParserException If there is any error parsing this node. + * @throws IOException If there is any error reading from the underlying input stream. + */ + protected Pair parseEvent( + XmlPullParser xpp, + String schemeIdUri, + String value, + long timescale, + ByteArrayOutputStream scratchOutputStream) + throws IOException, XmlPullParserException { + long id = parseLong(xpp, "id", 0); + long duration = parseLong(xpp, "duration", C.TIME_UNSET); + long presentationTime = parseLong(xpp, "presentationTime", 0); + long durationMs = Util.scaleLargeTimestamp(duration, C.MILLIS_PER_SECOND, timescale); + long presentationTimesUs = Util.scaleLargeTimestamp(presentationTime, C.MICROS_PER_SECOND, + timescale); + String messageData = parseString(xpp, "messageData", null); + byte[] eventObject = parseEventObject(xpp, scratchOutputStream); + return Pair.create( + presentationTimesUs, + buildEvent( + schemeIdUri, + value, + id, + durationMs, + messageData == null ? eventObject : Util.getUtf8Bytes(messageData))); + } + + /** + * Parses an event object. + * + * @param xpp The current xml parser. + * @param scratchOutputStream A {@link ByteArrayOutputStream} that's used when parsing the object. + * @return The serialized byte array. + * @throws XmlPullParserException If there is any error parsing this node. + * @throws IOException If there is any error reading from the underlying input stream. + */ + protected byte[] parseEventObject(XmlPullParser xpp, ByteArrayOutputStream scratchOutputStream) + throws XmlPullParserException, IOException { + scratchOutputStream.reset(); + XmlSerializer xmlSerializer = Xml.newSerializer(); + xmlSerializer.setOutput(scratchOutputStream, Charsets.UTF_8.name()); + // Start reading everything between and , and serialize them into an Xml + // byte array. + xpp.nextToken(); + while (!XmlPullParserUtil.isEndTag(xpp, "Event")) { + switch (xpp.getEventType()) { + case (XmlPullParser.START_DOCUMENT): + xmlSerializer.startDocument(null, false); + break; + case (XmlPullParser.END_DOCUMENT): + xmlSerializer.endDocument(); + break; + case (XmlPullParser.START_TAG): + xmlSerializer.startTag(xpp.getNamespace(), xpp.getName()); + for (int i = 0; i < xpp.getAttributeCount(); i++) { + xmlSerializer.attribute(xpp.getAttributeNamespace(i), xpp.getAttributeName(i), + xpp.getAttributeValue(i)); + } + break; + case (XmlPullParser.END_TAG): + xmlSerializer.endTag(xpp.getNamespace(), xpp.getName()); + break; + case (XmlPullParser.TEXT): + xmlSerializer.text(xpp.getText()); + break; + case (XmlPullParser.CDSECT): + xmlSerializer.cdsect(xpp.getText()); + break; + case (XmlPullParser.ENTITY_REF): + xmlSerializer.entityRef(xpp.getText()); + break; + case (XmlPullParser.IGNORABLE_WHITESPACE): + xmlSerializer.ignorableWhitespace(xpp.getText()); + break; + case (XmlPullParser.PROCESSING_INSTRUCTION): + xmlSerializer.processingInstruction(xpp.getText()); + break; + case (XmlPullParser.COMMENT): + xmlSerializer.comment(xpp.getText()); + break; + case (XmlPullParser.DOCDECL): + xmlSerializer.docdecl(xpp.getText()); + break; + default: // fall out + } + xpp.nextToken(); + } + xmlSerializer.flush(); + return scratchOutputStream.toByteArray(); + } + + protected EventMessage buildEvent( + String schemeIdUri, String value, long id, long durationMs, byte[] messageData) { + return new EventMessage(schemeIdUri, value, durationMs, id, messageData); + } + + protected List parseSegmentTimeline( + XmlPullParser xpp, long timescale, long periodDurationMs) + throws XmlPullParserException, IOException { + List segmentTimeline = new ArrayList<>(); + long startTime = 0; + long elementDuration = C.TIME_UNSET; + int elementRepeatCount = 0; + boolean havePreviousTimelineElement = false; + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "S")) { + long newStartTime = parseLong(xpp, "t", C.TIME_UNSET); + if (havePreviousTimelineElement) { + startTime = + addSegmentTimelineElementsToList( + segmentTimeline, + startTime, + elementDuration, + elementRepeatCount, + /* endTime= */ newStartTime); + } + if (newStartTime != C.TIME_UNSET) { + startTime = newStartTime; + } + elementDuration = parseLong(xpp, "d", C.TIME_UNSET); + elementRepeatCount = parseInt(xpp, "r", 0); + havePreviousTimelineElement = true; + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "SegmentTimeline")); + if (havePreviousTimelineElement) { + long periodDuration = Util.scaleLargeTimestamp(periodDurationMs, timescale, 1000); + addSegmentTimelineElementsToList( + segmentTimeline, + startTime, + elementDuration, + elementRepeatCount, + /* endTime= */ periodDuration); + } + return segmentTimeline; + } + + /** + * Adds timeline elements for one S tag to the segment timeline. + * + * @param startTime Start time of the first timeline element. + * @param elementDuration Duration of one timeline element. + * @param elementRepeatCount Number of timeline elements minus one. May be negative to indicate + * that the count is determined by the total duration and the element duration. + * @param endTime End time of the last timeline element for this S tag, or {@link C#TIME_UNSET} if + * unknown. Only needed if {@code repeatCount} is negative. + * @return Calculated next start time. + */ + private long addSegmentTimelineElementsToList( + List segmentTimeline, + long startTime, + long elementDuration, + int elementRepeatCount, + long endTime) { + int count = + elementRepeatCount >= 0 + ? 1 + elementRepeatCount + : (int) Util.ceilDivide(endTime - startTime, elementDuration); + for (int i = 0; i < count; i++) { + segmentTimeline.add(buildSegmentTimelineElement(startTime, elementDuration)); + startTime += elementDuration; + } + return startTime; + } + + protected SegmentTimelineElement buildSegmentTimelineElement(long startTime, long duration) { + return new SegmentTimelineElement(startTime, duration); + } + + @Nullable + protected UrlTemplate parseUrlTemplate( + XmlPullParser xpp, String name, @Nullable UrlTemplate defaultValue) { + String valueString = xpp.getAttributeValue(null, name); + if (valueString != null) { + return UrlTemplate.compile(valueString); + } + return defaultValue; + } + + protected RangedUri parseInitialization(XmlPullParser xpp) { + return parseRangedUrl(xpp, "sourceURL", "range"); + } + + protected RangedUri parseSegmentUrl(XmlPullParser xpp) { + return parseRangedUrl(xpp, "media", "mediaRange"); + } + + protected RangedUri parseRangedUrl(XmlPullParser xpp, String urlAttribute, + String rangeAttribute) { + String urlText = xpp.getAttributeValue(null, urlAttribute); + long rangeStart = 0; + long rangeLength = C.LENGTH_UNSET; + String rangeText = xpp.getAttributeValue(null, rangeAttribute); + if (rangeText != null) { + String[] rangeTextArray = rangeText.split("-"); + rangeStart = Long.parseLong(rangeTextArray[0]); + if (rangeTextArray.length == 2) { + rangeLength = Long.parseLong(rangeTextArray[1]) - rangeStart + 1; + } + } + return buildRangedUri(urlText, rangeStart, rangeLength); + } + + protected RangedUri buildRangedUri(String urlText, long rangeStart, long rangeLength) { + return new RangedUri(urlText, rangeStart, rangeLength); + } + + protected ProgramInformation parseProgramInformation(XmlPullParser xpp) + throws IOException, XmlPullParserException { + String title = null; + String source = null; + String copyright = null; + String moreInformationURL = parseString(xpp, "moreInformationURL", null); + String lang = parseString(xpp, "lang", null); + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "Title")) { + title = xpp.nextText(); + } else if (XmlPullParserUtil.isStartTag(xpp, "Source")) { + source = xpp.nextText(); + } else if (XmlPullParserUtil.isStartTag(xpp, "Copyright")) { + copyright = xpp.nextText(); + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "ProgramInformation")); + return new ProgramInformation(title, source, copyright, moreInformationURL, lang); + } + + /** + * Parses a Label element. + * + * @param xpp The parser from which to read. + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + * @return The parsed label. + */ + protected String parseLabel(XmlPullParser xpp) throws XmlPullParserException, IOException { + return parseText(xpp, "Label"); + } + + /** + * Parses a BaseURL element. + * + * @param xpp The parser from which to read. + * @param parentBaseUrl A base URL for resolving the parsed URL. + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + * @return The parsed and resolved URL. + */ + protected String parseBaseUrl(XmlPullParser xpp, String parentBaseUrl) + throws XmlPullParserException, IOException { + return UriUtil.resolve(parentBaseUrl, parseText(xpp, "BaseURL")); + } + + /** + * Parses the availabilityTimeOffset value and returns the parsed value or the parent value if it + * doesn't exist. + * + * @param xpp The parser from which to read. + * @param parentAvailabilityTimeOffsetUs The availability time offset of a parent element in + * microseconds. + * @return The parsed availabilityTimeOffset in microseconds. + */ + protected long parseAvailabilityTimeOffsetUs( + XmlPullParser xpp, long parentAvailabilityTimeOffsetUs) { + String value = xpp.getAttributeValue(/* namespace= */ null, "availabilityTimeOffset"); + if (value == null) { + return parentAvailabilityTimeOffsetUs; + } + if ("INF".equals(value)) { + return Long.MAX_VALUE; + } + return (long) (Float.parseFloat(value) * C.MICROS_PER_SECOND); + } + + // AudioChannelConfiguration parsing. + + protected int parseAudioChannelConfiguration(XmlPullParser xpp) + throws XmlPullParserException, IOException { + String schemeIdUri = parseString(xpp, "schemeIdUri", null); + int audioChannels; + switch (schemeIdUri) { + case "urn:mpeg:dash:23003:3:audio_channel_configuration:2011": + audioChannels = parseInt(xpp, "value", Format.NO_VALUE); + break; + case "urn:mpeg:mpegB:cicp:ChannelConfiguration": + audioChannels = parseMpegChannelConfiguration(xpp); + break; + case "tag:dolby.com,2014:dash:audio_channel_configuration:2011": + case "urn:dolby:dash:audio_channel_configuration:2011": + audioChannels = parseDolbyChannelConfiguration(xpp); + break; + default: + audioChannels = Format.NO_VALUE; + break; + } + do { + xpp.next(); + } while (!XmlPullParserUtil.isEndTag(xpp, "AudioChannelConfiguration")); + return audioChannels; + } + + // Selection flag parsing. + + protected int parseSelectionFlagsFromRoleDescriptors(List roleDescriptors) { + for (int i = 0; i < roleDescriptors.size(); i++) { + Descriptor descriptor = roleDescriptors.get(i); + if ("urn:mpeg:dash:role:2011".equalsIgnoreCase(descriptor.schemeIdUri) + && "main".equals(descriptor.value)) { + return C.SELECTION_FLAG_DEFAULT; + } + } + return 0; + } + + // Role and Accessibility parsing. + + @C.RoleFlags + protected int parseRoleFlagsFromRoleDescriptors(List roleDescriptors) { + @C.RoleFlags int result = 0; + for (int i = 0; i < roleDescriptors.size(); i++) { + Descriptor descriptor = roleDescriptors.get(i); + if ("urn:mpeg:dash:role:2011".equalsIgnoreCase(descriptor.schemeIdUri)) { + result |= parseDashRoleSchemeValue(descriptor.value); + } + } + return result; + } + + @C.RoleFlags + protected int parseRoleFlagsFromAccessibilityDescriptors( + List accessibilityDescriptors) { + @C.RoleFlags int result = 0; + for (int i = 0; i < accessibilityDescriptors.size(); i++) { + Descriptor descriptor = accessibilityDescriptors.get(i); + if ("urn:mpeg:dash:role:2011".equalsIgnoreCase(descriptor.schemeIdUri)) { + result |= parseDashRoleSchemeValue(descriptor.value); + } else if ("urn:tva:metadata:cs:AudioPurposeCS:2007" + .equalsIgnoreCase(descriptor.schemeIdUri)) { + result |= parseTvaAudioPurposeCsValue(descriptor.value); + } + } + return result; + } + + @C.RoleFlags + protected int parseRoleFlagsFromProperties(List accessibilityDescriptors) { + @C.RoleFlags int result = 0; + for (int i = 0; i < accessibilityDescriptors.size(); i++) { + Descriptor descriptor = accessibilityDescriptors.get(i); + if ("http://dashif.org/guidelines/trickmode".equalsIgnoreCase(descriptor.schemeIdUri)) { + result |= C.ROLE_FLAG_TRICK_PLAY; + } + } + return result; + } + + @C.RoleFlags + protected int parseDashRoleSchemeValue(@Nullable String value) { + if (value == null) { + return 0; + } + switch (value) { + case "main": + return C.ROLE_FLAG_MAIN; + case "alternate": + return C.ROLE_FLAG_ALTERNATE; + case "supplementary": + return C.ROLE_FLAG_SUPPLEMENTARY; + case "commentary": + return C.ROLE_FLAG_COMMENTARY; + case "dub": + return C.ROLE_FLAG_DUB; + case "emergency": + return C.ROLE_FLAG_EMERGENCY; + case "caption": + return C.ROLE_FLAG_CAPTION; + case "subtitle": + return C.ROLE_FLAG_SUBTITLE; + case "sign": + return C.ROLE_FLAG_SIGN; + case "description": + return C.ROLE_FLAG_DESCRIBES_VIDEO; + case "enhanced-audio-intelligibility": + return C.ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY; + default: + return 0; + } + } + + @C.RoleFlags + protected int parseTvaAudioPurposeCsValue(@Nullable String value) { + if (value == null) { + return 0; + } + switch (value) { + case "1": // Audio description for the visually impaired. + return C.ROLE_FLAG_DESCRIBES_VIDEO; + case "2": // Audio description for the hard of hearing. + return C.ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY; + case "3": // Supplemental commentary. + return C.ROLE_FLAG_SUPPLEMENTARY; + case "4": // Director's commentary. + return C.ROLE_FLAG_COMMENTARY; + case "6": // Main programme audio. + return C.ROLE_FLAG_MAIN; + default: + return 0; + } + } + + // Utility methods. + + /** + * If the provided {@link XmlPullParser} is currently positioned at the start of a tag, skips + * forward to the end of that tag. + * + * @param xpp The {@link XmlPullParser}. + * @throws XmlPullParserException If an error occurs parsing the stream. + * @throws IOException If an error occurs reading the stream. + */ + public static void maybeSkipTag(XmlPullParser xpp) throws IOException, XmlPullParserException { + if (!XmlPullParserUtil.isStartTag(xpp)) { + return; + } + int depth = 1; + while (depth != 0) { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp)) { + depth++; + } else if (XmlPullParserUtil.isEndTag(xpp)) { + depth--; + } + } + } + + /** + * Removes unnecessary {@link SchemeData}s with null {@link SchemeData#data}. + */ + private static void filterRedundantIncompleteSchemeDatas(ArrayList schemeDatas) { + for (int i = schemeDatas.size() - 1; i >= 0; i--) { + SchemeData schemeData = schemeDatas.get(i); + if (!schemeData.hasData()) { + for (int j = 0; j < schemeDatas.size(); j++) { + if (schemeDatas.get(j).canReplace(schemeData)) { + // schemeData is incomplete, but there is another matching SchemeData which does contain + // data, so we remove the incomplete one. + schemeDatas.remove(i); + break; + } + } + } + } + } + + /** + * Derives a sample mimeType from a container mimeType and codecs attribute. + * + * @param containerMimeType The mimeType of the container. + * @param codecs The codecs attribute. + * @return The derived sample mimeType, or null if it could not be derived. + */ + @Nullable + private static String getSampleMimeType( + @Nullable String containerMimeType, @Nullable String codecs) { + if (MimeTypes.isAudio(containerMimeType)) { + return MimeTypes.getAudioMediaMimeType(codecs); + } else if (MimeTypes.isVideo(containerMimeType)) { + return MimeTypes.getVideoMediaMimeType(codecs); + } else if (MimeTypes.isText(containerMimeType)) { + if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { + // RawCC is special because it's a text specific container format. + return MimeTypes.getTextMediaMimeType(codecs); + } + // All other text types are raw formats. + return containerMimeType; + } else if (MimeTypes.APPLICATION_MP4.equals(containerMimeType)) { + @Nullable String mimeType = MimeTypes.getMediaMimeType(codecs); + return MimeTypes.TEXT_VTT.equals(mimeType) ? MimeTypes.APPLICATION_MP4VTT : mimeType; + } + return null; + } + + /** + * Checks two languages for consistency, returning the consistent language, or throwing an {@link + * IllegalStateException} if the languages are inconsistent. + * + *

Two languages are consistent if they are equal, or if one is null. + * + * @param firstLanguage The first language. + * @param secondLanguage The second language. + * @return The consistent language. + */ + @Nullable + private static String checkLanguageConsistency( + @Nullable String firstLanguage, @Nullable String secondLanguage) { + if (firstLanguage == null) { + return secondLanguage; + } else if (secondLanguage == null) { + return firstLanguage; + } else { + Assertions.checkState(firstLanguage.equals(secondLanguage)); + return firstLanguage; + } + } + + /** + * Checks two adaptation set content types for consistency, returning the consistent type, or + * throwing an {@link IllegalStateException} if the types are inconsistent. + *

+ * Two types are consistent if they are equal, or if one is {@link C#TRACK_TYPE_UNKNOWN}. + * Where one of the types is {@link C#TRACK_TYPE_UNKNOWN}, the other is returned. + * + * @param firstType The first type. + * @param secondType The second type. + * @return The consistent type. + */ + private static int checkContentTypeConsistency(int firstType, int secondType) { + if (firstType == C.TRACK_TYPE_UNKNOWN) { + return secondType; + } else if (secondType == C.TRACK_TYPE_UNKNOWN) { + return firstType; + } else { + Assertions.checkState(firstType == secondType); + return firstType; + } + } + + /** + * Parses a {@link Descriptor} from an element. + * + * @param xpp The parser from which to read. + * @param tag The tag of the element being parsed. + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + * @return The parsed {@link Descriptor}. + */ + protected static Descriptor parseDescriptor(XmlPullParser xpp, String tag) + throws XmlPullParserException, IOException { + String schemeIdUri = parseString(xpp, "schemeIdUri", ""); + String value = parseString(xpp, "value", null); + String id = parseString(xpp, "id", null); + do { + xpp.next(); + } while (!XmlPullParserUtil.isEndTag(xpp, tag)); + return new Descriptor(schemeIdUri, value, id); + } + + protected static int parseCea608AccessibilityChannel( + List accessibilityDescriptors) { + for (int i = 0; i < accessibilityDescriptors.size(); i++) { + Descriptor descriptor = accessibilityDescriptors.get(i); + if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri) + && descriptor.value != null) { + Matcher accessibilityValueMatcher = CEA_608_ACCESSIBILITY_PATTERN.matcher(descriptor.value); + if (accessibilityValueMatcher.matches()) { + return Integer.parseInt(accessibilityValueMatcher.group(1)); + } else { + Log.w(TAG, "Unable to parse CEA-608 channel number from: " + descriptor.value); + } + } + } + return Format.NO_VALUE; + } + + protected static int parseCea708AccessibilityChannel( + List accessibilityDescriptors) { + for (int i = 0; i < accessibilityDescriptors.size(); i++) { + Descriptor descriptor = accessibilityDescriptors.get(i); + if ("urn:scte:dash:cc:cea-708:2015".equals(descriptor.schemeIdUri) + && descriptor.value != null) { + Matcher accessibilityValueMatcher = CEA_708_ACCESSIBILITY_PATTERN.matcher(descriptor.value); + if (accessibilityValueMatcher.matches()) { + return Integer.parseInt(accessibilityValueMatcher.group(1)); + } else { + Log.w(TAG, "Unable to parse CEA-708 service block number from: " + descriptor.value); + } + } + } + return Format.NO_VALUE; + } + + protected static String parseEac3SupplementalProperties(List supplementalProperties) { + for (int i = 0; i < supplementalProperties.size(); i++) { + Descriptor descriptor = supplementalProperties.get(i); + String schemeIdUri = descriptor.schemeIdUri; + if (("tag:dolby.com,2018:dash:EC3_ExtensionType:2018".equals(schemeIdUri) + && "JOC".equals(descriptor.value)) + || ("tag:dolby.com,2014:dash:DolbyDigitalPlusExtensionType:2014".equals(schemeIdUri) + && "ec+3".equals(descriptor.value))) { + return MimeTypes.AUDIO_E_AC3_JOC; + } + } + return MimeTypes.AUDIO_E_AC3; + } + + protected static float parseFrameRate(XmlPullParser xpp, float defaultValue) { + float frameRate = defaultValue; + String frameRateAttribute = xpp.getAttributeValue(null, "frameRate"); + if (frameRateAttribute != null) { + Matcher frameRateMatcher = FRAME_RATE_PATTERN.matcher(frameRateAttribute); + if (frameRateMatcher.matches()) { + int numerator = Integer.parseInt(frameRateMatcher.group(1)); + String denominatorString = frameRateMatcher.group(2); + if (!TextUtils.isEmpty(denominatorString)) { + frameRate = (float) numerator / Integer.parseInt(denominatorString); + } else { + frameRate = numerator; + } + } + } + return frameRate; + } + + protected static long parseDuration(XmlPullParser xpp, String name, long defaultValue) { + String value = xpp.getAttributeValue(null, name); + if (value == null) { + return defaultValue; + } else { + return Util.parseXsDuration(value); + } + } + + protected static long parseDateTime(XmlPullParser xpp, String name, long defaultValue) + throws ParserException { + String value = xpp.getAttributeValue(null, name); + if (value == null) { + return defaultValue; + } else { + return Util.parseXsDateTime(value); + } + } + + protected static String parseText(XmlPullParser xpp, String label) + throws XmlPullParserException, IOException { + String text = ""; + do { + xpp.next(); + if (xpp.getEventType() == XmlPullParser.TEXT) { + text = xpp.getText(); + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, label)); + return text; + } + + protected static int parseInt(XmlPullParser xpp, String name, int defaultValue) { + String value = xpp.getAttributeValue(null, name); + if (value == null || !TextUtils.isDigitsOnly(value)) { + return defaultValue; + } + return Integer.parseInt(value); + } + + protected static long parseLong(XmlPullParser xpp, String name, long defaultValue) { + String value = xpp.getAttributeValue(null, name); + boolean isFloat = false; + if (value !=null && value.contains(".")) { + value = value.split("\\.")[0]; + isFloat = true; + } + long longValue = (value == null) ? defaultValue : Long.parseLong(value); + if (isFloat) { + return ++longValue; + } + return longValue; + } + + protected static float parseFloat(XmlPullParser xpp, String name, float defaultValue) { + String value = xpp.getAttributeValue(null, name); + return value == null ? defaultValue : Float.parseFloat(value); + } + + protected static String parseString(XmlPullParser xpp, String name, String defaultValue) { + String value = xpp.getAttributeValue(null, name); + return value == null ? defaultValue : value; + } + + /** + * Parses the number of channels from the value attribute of an AudioElementConfiguration with + * schemeIdUri "urn:mpeg:mpegB:cicp:ChannelConfiguration", as defined by ISO 23001-8 clause 8.1. + * + * @param xpp The parser from which to read. + * @return The parsed number of channels, or {@link Format#NO_VALUE} if the channel count could + * not be parsed. + */ + protected static int parseMpegChannelConfiguration(XmlPullParser xpp) { + int index = parseInt(xpp, "value", C.INDEX_UNSET); + return 0 <= index && index < MPEG_CHANNEL_CONFIGURATION_MAPPING.length + ? MPEG_CHANNEL_CONFIGURATION_MAPPING[index] + : Format.NO_VALUE; + } + + /** + * Parses the number of channels from the value attribute of an AudioElementConfiguration with + * schemeIdUri "tag:dolby.com,2014:dash:audio_channel_configuration:2011", as defined by table E.5 + * in ETSI TS 102 366, or the legacy schemeIdUri + * "urn:dolby:dash:audio_channel_configuration:2011". + * + * @param xpp The parser from which to read. + * @return The parsed number of channels, or {@link Format#NO_VALUE} if the channel count could + * not be parsed. + */ + protected static int parseDolbyChannelConfiguration(XmlPullParser xpp) { + String value = Util.toLowerInvariant(xpp.getAttributeValue(null, "value")); + if (value == null) { + return Format.NO_VALUE; + } + switch (value) { + case "4000": + return 1; + case "a000": + return 2; + case "f801": + return 6; + case "fa01": + return 8; + default: + return Format.NO_VALUE; + } + } + + protected static long parseLastSegmentNumberSupplementalProperty( + List supplementalProperties) { + for (int i = 0; i < supplementalProperties.size(); i++) { + Descriptor descriptor = supplementalProperties.get(i); + if ("http://dashif.org/guidelines/last-segment-number" + .equalsIgnoreCase(descriptor.schemeIdUri)) { + return Long.parseLong(descriptor.value); + } + } + return C.INDEX_UNSET; + } + + private static long getFinalAvailabilityTimeOffset( + long baseUrlAvailabilityTimeOffsetUs, long segmentBaseAvailabilityTimeOffsetUs) { + long availabilityTimeOffsetUs = segmentBaseAvailabilityTimeOffsetUs; + if (availabilityTimeOffsetUs == C.TIME_UNSET) { + // Fall back to BaseURL values if no SegmentBase specifies an offset. + availabilityTimeOffsetUs = baseUrlAvailabilityTimeOffsetUs; + } + if (availabilityTimeOffsetUs == Long.MAX_VALUE) { + // Replace INF value with TIME_UNSET to specify that all segments are available immediately. + availabilityTimeOffsetUs = C.TIME_UNSET; + } + return availabilityTimeOffsetUs; + } + + /** A parsed Representation element. */ + protected static final class RepresentationInfo { + + public final Format format; + public final String baseUrl; + public final SegmentBase segmentBase; + @Nullable public final String drmSchemeType; + public final ArrayList drmSchemeDatas; + public final ArrayList inbandEventStreams; + public final long revisionId; + + public RepresentationInfo( + Format format, + String baseUrl, + SegmentBase segmentBase, + @Nullable String drmSchemeType, + ArrayList drmSchemeDatas, + ArrayList inbandEventStreams, + long revisionId) { + this.format = format; + this.baseUrl = baseUrl; + this.segmentBase = segmentBase; + this.drmSchemeType = drmSchemeType; + this.drmSchemeDatas = drmSchemeDatas; + this.inbandEventStreams = inbandEventStreams; + this.revisionId = revisionId; + } + + } + +} diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java index e0783f71f..58d482c0f 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java @@ -55,6 +55,8 @@ import com.kaltura.android.exoplayer2.source.dash.DashMediaSource; import com.kaltura.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.kaltura.android.exoplayer2.source.dash.manifest.DashManifest; +import com.kaltura.android.exoplayer2.source.dash.manifest.DashManifestParser; +import com.kaltura.android.exoplayer2.source.dash.manifest.DashManifestParserForThumbnail; import com.kaltura.android.exoplayer2.source.hls.HlsMediaSource; import com.kaltura.android.exoplayer2.trackselection.DefaultTrackSelector; import com.kaltura.android.exoplayer2.trackselection.TrackSelectionArray; @@ -436,6 +438,7 @@ private MediaSource buildInternalExoMediaSource(MediaItem mediaItem, PKMediaSour mediaSource = new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(dataSourceFactory), teedDtaSourceFactory) .setDrmSessionManager(sourceConfig.mediaSource.hasDrmParams() ? drmSessionManager : DrmSessionManager.DRM_UNSUPPORTED) + .setManifestParser(new DashManifestParserForThumbnail()) .createMediaSource(mediaItem); break; From 61cba039634697ee60bc31ad4868a69982209eab Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Mon, 22 Feb 2021 12:36:36 +0200 Subject: [PATCH 07/34] fix npe - last selected index for image was not set. by default the first image track is selected --- .../playkit/player/TrackSelectionHelper.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java index 69c146cc2..8977bf855 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java @@ -338,6 +338,12 @@ private PKTracks buildTracks(List rawImageTracks) { formatThumbnailInfo.imageTemplateUrl )); } + + if (NONE.equals(requestedChangeTrackIds[TRACK_TYPE_IMAGE])) { + log.d("Image track changed to: " + requestedChangeTrackIds[TRACK_TYPE_IMAGE]); + lastSelectedTrackIds[TRACK_TYPE_IMAGE] = imageTracks.get(0).getUniqueId(); + tracksInfoListener.onImageTrackChanged(); + } } //add disable option to the text tracks. @@ -806,7 +812,6 @@ protected void changeTrack(String uniqueId) { return; } - int[] uniqueTrackId = validateUniqueId(uniqueId); int rendererIndex = uniqueTrackId[RENDERER_INDEX]; @@ -1191,6 +1196,10 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { break; } } + + if (imageTrack == null) { + return null; + } long seq = (long)Math.floor(positionMS / imageTrack.getSegmentDuration()); double offset = positionMS % imageTrack.getSegmentDuration(); @@ -1488,12 +1497,6 @@ protected void notifyAboutTrackChange(TrackSelectionArray trackSelections) { lastSelectedTrackIds[TRACK_TYPE_TEXT] = requestedChangeTrackIds[TRACK_TYPE_TEXT]; tracksInfoListener.onTextTrackChanged(); } - - if (shouldNotifyAboutTrackChanged(TRACK_TYPE_IMAGE)) { - log.d("Image track changed to: " + requestedChangeTrackIds[TRACK_TYPE_IMAGE]); - lastSelectedTrackIds[TRACK_TYPE_IMAGE] = requestedChangeTrackIds[TRACK_TYPE_IMAGE]; - tracksInfoListener.onImageTrackChanged(); - } } private boolean shouldNotifyAboutTrackChanged(int renderType) { From b1b44ae527bee1b89fad99892be301224806c310 Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Tue, 23 Feb 2021 08:55:57 +0200 Subject: [PATCH 08/34] test: Expose for vod Map getVodThumbnailInfo(); --- .../main/java/com/kaltura/playkit/Player.java | 8 +- .../kaltura/playkit/PlayerDecoratorBase.java | 11 ++- .../kaltura/playkit/PlayerEngineWrapper.java | 11 ++- .../playkit/player/ExoPlayerWrapper.java | 36 +++++-- .../playkit/player/PlayerController.java | 13 +++ .../kaltura/playkit/player/PlayerEngine.java | 7 ++ .../playkit/player/TrackSelectionHelper.java | 93 ++++++++++++++++++- .../player/thumbnail/ImageRangeInfo.java | 13 +++ .../player/thumbnail/ThumbnailDimensions.java | 11 +++ .../player/{ => thumbnail}/ThumbnailInfo.java | 2 +- .../player/thumbnail/ThumbnailVodInfo.java | 65 +++++++++++++ 11 files changed, 252 insertions(+), 18 deletions(-) create mode 100644 playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ImageRangeInfo.java create mode 100644 playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailDimensions.java rename playkit/src/main/java/com/kaltura/playkit/player/{ => thumbnail}/ThumbnailInfo.java (94%) create mode 100644 playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java diff --git a/playkit/src/main/java/com/kaltura/playkit/Player.java b/playkit/src/main/java/com/kaltura/playkit/Player.java index 011b5754b..88999b7c2 100644 --- a/playkit/src/main/java/com/kaltura/playkit/Player.java +++ b/playkit/src/main/java/com/kaltura/playkit/Player.java @@ -12,6 +12,8 @@ package com.kaltura.playkit; +import android.graphics.Rect; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -21,13 +23,15 @@ import com.kaltura.playkit.player.PKMaxVideoSize; import com.kaltura.playkit.player.PlayerView; import com.kaltura.playkit.player.SubtitleStyleSettings; -import com.kaltura.playkit.player.ThumbnailInfo; +import com.kaltura.playkit.player.thumbnail.ImageRangeInfo; +import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; import com.kaltura.playkit.player.VideoCodecSettings; import com.kaltura.playkit.player.AudioCodecSettings; import com.kaltura.playkit.player.vr.VRSettings; import com.kaltura.playkit.utils.Consts; import java.util.List; +import java.util.Map; @SuppressWarnings("unused") public interface Player { @@ -527,6 +531,8 @@ interface Settings { */ ThumbnailInfo getThumbnailInfo(long positionMS); + Map getVodThumbnailInfo(); + /** * Generic getters for playkit controllers. * diff --git a/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java b/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java index 1abcafb87..768c60793 100644 --- a/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java +++ b/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java @@ -12,15 +12,19 @@ package com.kaltura.playkit; +import android.graphics.Rect; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.kaltura.playkit.player.PKAspectRatioResizeMode; import com.kaltura.playkit.player.PlayerView; import com.kaltura.playkit.player.SubtitleStyleSettings; -import com.kaltura.playkit.player.ThumbnailInfo; +import com.kaltura.playkit.player.thumbnail.ImageRangeInfo; +import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; import java.util.List; +import java.util.Map; public class PlayerDecoratorBase implements Player { @@ -99,6 +103,11 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { return player.getThumbnailInfo(positionMS); } + @Override + public Map getVodThumbnailInfo() { + return player.getVodThumbnailInfo(); + } + @Override public void play() { player.play(); diff --git a/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java b/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java index 3901526fa..2c6c7323e 100644 --- a/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java @@ -1,5 +1,7 @@ package com.kaltura.playkit; +import android.graphics.Rect; + import com.kaltura.playkit.player.BaseTrack; import com.kaltura.playkit.player.PKAspectRatioResizeMode; import com.kaltura.playkit.player.PKMediaSourceConfig; @@ -8,10 +10,12 @@ import com.kaltura.playkit.player.PlayerView; import com.kaltura.playkit.player.Profiler; import com.kaltura.playkit.player.SubtitleStyleSettings; -import com.kaltura.playkit.player.ThumbnailInfo; +import com.kaltura.playkit.player.thumbnail.ImageRangeInfo; +import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; import com.kaltura.playkit.player.metadata.PKMetadata; import java.util.List; +import java.util.Map; public class PlayerEngineWrapper implements PlayerEngine { @@ -197,6 +201,11 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { return playerEngine.getThumbnailInfo(positionMS); } + @Override + public Map getVodThumbnailInfo() { + return playerEngine.getVodThumbnailInfo(); + } + @Override public void setProfiler(Profiler profiler) { this.playerEngine.setProfiler(profiler); diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java index 58d482c0f..de3ef8099 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java @@ -13,9 +13,9 @@ package com.kaltura.playkit.player; import android.content.Context; +import android.graphics.Rect; import android.media.MediaCodec; import android.net.Uri; -import android.os.Build; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; @@ -29,7 +29,6 @@ import com.kaltura.android.exoplayer2.ExoPlayerLibraryInfo; import com.kaltura.android.exoplayer2.LoadControl; import com.kaltura.android.exoplayer2.MediaItem; -import com.kaltura.android.exoplayer2.ParserException; import com.kaltura.android.exoplayer2.PlaybackParameters; import com.kaltura.android.exoplayer2.Player; import com.kaltura.android.exoplayer2.SimpleExoPlayer; @@ -55,7 +54,6 @@ import com.kaltura.android.exoplayer2.source.dash.DashMediaSource; import com.kaltura.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.kaltura.android.exoplayer2.source.dash.manifest.DashManifest; -import com.kaltura.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.kaltura.android.exoplayer2.source.dash.manifest.DashManifestParserForThumbnail; import com.kaltura.android.exoplayer2.source.hls.HlsMediaSource; import com.kaltura.android.exoplayer2.trackselection.DefaultTrackSelector; @@ -77,16 +75,17 @@ import com.kaltura.playkit.drm.DrmCallback; import com.kaltura.playkit.player.metadata.MetadataConverter; import com.kaltura.playkit.player.metadata.PKMetadata; +import com.kaltura.playkit.player.thumbnail.ImageRangeInfo; +import com.kaltura.playkit.player.thumbnail.ThumbnailVodInfo; +import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; import com.kaltura.playkit.utils.Consts; import com.kaltura.playkit.utils.NativeCookieJarBridge; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -1511,15 +1510,32 @@ public boolean isLive() { @Override public ThumbnailInfo getThumbnailInfo(long positionMS) { - if(isLive()) { - Timeline timeline = player.getCurrentTimeline(); - if (!timeline.isEmpty()) { - positionMS -= timeline.getPeriod(player.getCurrentPeriodIndex(), new Timeline.Period()).getPositionInWindowMs(); + log.v("getThumbnailInfo positionMS = " + positionMS); + if (assertPlayerIsNotNull("getThumbnailInfo()")) { + if (isLive()) { + Timeline timeline = player.getCurrentTimeline(); + if (!timeline.isEmpty()) { + positionMS -= timeline.getPeriod(player.getCurrentPeriodIndex(), new Timeline.Period()).getPositionInWindowMs(); + } + } + return trackSelectionHelper.getThumbnailInfo(positionMS); + } + return null; + } + + @Override + public Map getVodThumbnailInfo() { + log.v("getVodThumbnailInfo"); + if (assertPlayerIsNotNull("getVodThumbnailInfo()")) { + long playerDuration = player.getDuration(); + if (playerDuration > 0) { + return trackSelectionHelper.getVodThumbnailInfo(playerDuration); } } - return trackSelectionHelper.getThumbnailInfo(positionMS); + return null; } + private void closeProfilerSession() { profiler.onSessionFinished(); } diff --git a/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java b/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java index 2fa8e5a43..dd451b878 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java @@ -13,6 +13,7 @@ package com.kaltura.playkit.player; import android.content.Context; +import android.graphics.Rect; import android.os.Handler; import android.os.Looper; @@ -36,10 +37,13 @@ import com.kaltura.playkit.ads.AdController; import com.kaltura.playkit.ads.AdsPlayerEngineWrapper; import com.kaltura.playkit.player.metadata.URIConnectionAcquiredInfo; +import com.kaltura.playkit.player.thumbnail.ImageRangeInfo; +import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; import com.kaltura.playkit.utils.Consts; import java.io.IOException; import java.util.List; +import java.util.Map; import java.util.UUID; import static com.kaltura.playkit.utils.Consts.MILLISECONDS_MULTIPLIER; @@ -670,6 +674,15 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { return null; } + @Override + public Map getVodThumbnailInfo() { + log.v("getVodThumbnailInfo"); + if (assertPlayerIsNotNull("getVodThumbnailInfo()")) { + return player.getVodThumbnailInfo(); + } + return null; + } + @Override public void updateSubtitleStyle(SubtitleStyleSettings subtitleStyleSettings) { log.v("updateSubtitleStyle"); diff --git a/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java b/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java index 1f5f3978b..4216b41a9 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java @@ -12,6 +12,8 @@ package com.kaltura.playkit.player; +import android.graphics.Rect; + import com.kaltura.playkit.PKController; import com.kaltura.playkit.PKError; import com.kaltura.playkit.PlaybackInfo; @@ -19,10 +21,13 @@ import com.kaltura.playkit.PlayerState; import com.kaltura.playkit.player.metadata.PKMetadata; import com.kaltura.playkit.player.metadata.URIConnectionAcquiredInfo; +import com.kaltura.playkit.player.thumbnail.ImageRangeInfo; +import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; import com.kaltura.playkit.utils.Consts; import java.io.IOException; import java.util.List; +import java.util.Map; /** @@ -288,6 +293,8 @@ default void updateSurfaceAspectRatioResizeMode(PKAspectRatioResizeMode resizeMo default ThumbnailInfo getThumbnailInfo(long positionMS) { return null; } + default Map getVodThumbnailInfo() { return null; } + interface EventListener { void onEvent(PlayerEvent.Type event); } diff --git a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java index 8977bf855..85ad4d4a7 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java @@ -14,6 +14,7 @@ import android.content.Context; +import android.graphics.Rect; import android.text.TextUtils; import androidx.annotation.NonNull; @@ -40,6 +41,9 @@ import com.kaltura.playkit.PKSubtitlePreference; import com.kaltura.playkit.PKTrackConfig; import com.kaltura.playkit.PKVideoCodec; +import com.kaltura.playkit.player.thumbnail.ImageRangeInfo; +import com.kaltura.playkit.player.thumbnail.ThumbnailVodInfo; +import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; import com.kaltura.playkit.utils.Consts; import java.util.ArrayList; @@ -47,6 +51,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -1196,7 +1201,7 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { break; } } - + if (imageTrack == null) { return null; } @@ -1212,12 +1217,87 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { long imageRealUrlTime = ((seqIdx - 1) * imageTrack.getSegmentDuration()); String realImageUrl = imageTrack.getImageTemplateUrl().replace("$Number$", String.valueOf(seqIdx)).replace("$Time$", String.valueOf(imageRealUrlTime)); - //int imageXColIndex = imageX / imageWidth; - //int imageYRowIndex = imageY / imageHeight; - //Rect rect = new Rect(imageX, imageY, imageXColIndex * imageWidth + imageWidth, imageYRowIndex * imageHeight + imageHeight); return new ThumbnailInfo(realImageUrl, imageX, imageY, imageWidth, imageHeight); } + public Map getVodThumbnailInfo(long mediaDurationMS) { + if (imageTracks.isEmpty()) { + return null; + } + + ImageTrack imageTrack = null; + for (int index = 0; index < imageTracks.size() ; index++) { + if (imageTracks.get(index).getUniqueId().equals(lastSelectedTrackIds[TRACK_TYPE_IMAGE])) { + imageTrack = imageTracks.get(index); + break; + } + } + + if (imageTrack == null) { + return null; + } + + Map imageRangeRectMap = new LinkedHashMap<>(); + + boolean isCatchup = false; + final long segmentDuration = imageTrack.getSegmentDuration(); + + int maxIndex = (int) Math.ceil((mediaDurationMS * 1.0) / segmentDuration); + if (imageTrack.getTilesVertical() == 1 && imageTrack.getTilesHorizontal() == 1) { + maxIndex = (int) Math.ceil((maxIndex) * ((segmentDuration / 1000.0) / 1)); + } + + if (maxIndex < imageTrack.getStartNumber()) { + isCatchup = true; + } + + ThumbnailVodInfo imageData = null; + long forLoopStartNumber; + long forLoopEndNumber; + if (isCatchup) { + long rangeValueStart = imageTrack.getStartNumber(); + long rangeValueEnd = imageTrack.getEndNumber() == -1 ? segmentDuration : imageTrack.getEndNumber(); + + + maxIndex += imageTrack.getStartNumber(); + rangeValueEnd += imageTrack.getStartNumber(); + + for (long index = imageTrack.getStartNumber(); index <= maxIndex; index++) { + + forLoopStartNumber = rangeValueStart; + long indexValue = index; + imageData = new ThumbnailVodInfo(indexValue, imageTrack, mediaDurationMS, forLoopStartNumber, isCatchup); + if (imageData != null) { + imageRangeRectMap.putAll(imageData.getImageRangeRectMap()); + } + rangeValueStart = rangeValueStart + segmentDuration; + if (rangeValueEnd != -1) { + rangeValueEnd += segmentDuration; + } + } + } else { + long rangeValueStart = imageTrack.getStartNumber(); + long rangeValueEnd = imageTrack.getEndNumber() == -1 ? segmentDuration : imageTrack.getEndNumber(); + + for (long index = imageTrack.getStartNumber(); index <= maxIndex; index++) { + + forLoopStartNumber = rangeValueStart; + long indexValue = index; + + imageData = new ThumbnailVodInfo(indexValue, imageTrack, mediaDurationMS, forLoopStartNumber, isCatchup); + if (imageData != null) { + imageRangeRectMap.putAll(imageData.getImageRangeRectMap()); + } + rangeValueStart = 1 + (index * segmentDuration); + if (rangeValueEnd != -1) { + rangeValueEnd += segmentDuration; + } + } + } + return imageRangeRectMap; + } + + /** * Checks if adaptive track for the specified group was created. @@ -1777,4 +1857,9 @@ public static boolean isCodecSupported(@NonNull String codecs, @Nullable TrackTy return PKCodecSupport.hasDecoder(codecs, false, allowSoftware); } } + + + + + } diff --git a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ImageRangeInfo.java b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ImageRangeInfo.java new file mode 100644 index 000000000..028f6e7ad --- /dev/null +++ b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ImageRangeInfo.java @@ -0,0 +1,13 @@ +package com.kaltura.playkit.player.thumbnail; + +public class ImageRangeInfo { + String imagelUrl; + long startPosition; + long endPosition; + + public ImageRangeInfo(String imagelUrl, long startPosition, long endPosition) { + this.imagelUrl = imagelUrl; + this.startPosition = startPosition; + this.endPosition = endPosition; + } +} diff --git a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailDimensions.java b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailDimensions.java new file mode 100644 index 000000000..23b2e37cb --- /dev/null +++ b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailDimensions.java @@ -0,0 +1,11 @@ +package com.kaltura.playkit.player.thumbnail; + +public class ThumbnailDimensions { + int width; + int height; + + public ThumbnailDimensions(int width, int height) { + this.width = width; + this.height = height; + } +} \ No newline at end of file diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ThumbnailInfo.java b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailInfo.java similarity index 94% rename from playkit/src/main/java/com/kaltura/playkit/player/ThumbnailInfo.java rename to playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailInfo.java index 105c7514e..f92248f76 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/ThumbnailInfo.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailInfo.java @@ -1,4 +1,4 @@ -package com.kaltura.playkit.player; +package com.kaltura.playkit.player.thumbnail; public class ThumbnailInfo { private String url; // url of the image that contains the thumbnail slice diff --git a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java new file mode 100644 index 000000000..077979395 --- /dev/null +++ b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java @@ -0,0 +1,65 @@ +package com.kaltura.playkit.player.thumbnail; + +import android.graphics.Rect; +import android.util.Log; + +import com.kaltura.playkit.player.ImageTrack; + +import java.util.LinkedHashMap; +import java.util.Map; + + +public class ThumbnailVodInfo { + + Map imageRangeRectMap; + + public Map getImageRangeRectMap() { + return imageRangeRectMap; + } + + public ThumbnailVodInfo(long imageUrlIndex, ImageTrack imageTrack, long mediaDurationMS, long startNumber, boolean isCatchup) { + + long imageMultiplier = imageUrlIndex <= 0 ? 0 : imageUrlIndex - 1; + long imageRealUrlTime = (imageMultiplier * imageTrack.getSegmentDuration()); + + if (isCatchup) { + imageMultiplier = imageTrack.getStartNumber() - imageUrlIndex <= 0 ? 0 : startNumber - imageUrlIndex - 1; + imageRealUrlTime = startNumber + (imageMultiplier * imageTrack.getSegmentDuration()); + } + String realImageUrl = imageTrack.getImageTemplateUrl().replace("$Number$", String.valueOf(imageUrlIndex)).replace("$Time$", String.valueOf(imageRealUrlTime)); + + long singleImageDuration = (long) Math.ceil(imageTrack.getSegmentDuration() / (imageTrack.getTilesHorizontal() * imageTrack.getTilesVertical())); + + + imageRangeRectMap = new LinkedHashMap<>(); + long rangeStart = startNumber == 1 ? 0 : startNumber; + long rangeEnd = (((imageMultiplier * imageTrack.getSegmentDuration()) + singleImageDuration) - 1); + long diff = 0; + if (rangeStart > rangeEnd) { + rangeEnd = rangeStart + rangeEnd; + } + diff = rangeEnd - rangeStart; + int widthPerTile = imageTrack.getWidth() / imageTrack.getTilesHorizontal(); + int heightPerTile = imageTrack.getHeight() / imageTrack.getTilesVertical(); + for (int rowIndex = 0; rowIndex < imageTrack.getTilesVertical(); rowIndex++) { + for (int colIndex = 0; colIndex < imageTrack.getTilesHorizontal(); colIndex++) { + //Log.d("GILAD BY POSITION THUMB", "[" + rowIndex + "," + colIndex + "] = [" + rangeStart + "," + rangeEnd + "]"); + + ImageRangeInfo imageRangeInfo = new ImageRangeInfo(realImageUrl, rangeStart, rangeEnd); + Rect rect = + new Rect((colIndex * widthPerTile), + rowIndex * heightPerTile, + (colIndex * widthPerTile + widthPerTile), + rowIndex * heightPerTile + heightPerTile); + + if (rangeEnd - diff > mediaDurationMS + imageTrack.getStartNumber()) { + continue; + } + imageRangeRectMap.put(imageRangeInfo, rect); + rangeStart += singleImageDuration; + rangeEnd = rangeStart + diff; + } + } + Log.d("GILAD THUMB", "--------------------------"); + } +} \ No newline at end of file From 910c3cbcb7769b19ec81f99423d7bb431d7b9edc Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Tue, 23 Feb 2021 09:36:20 +0200 Subject: [PATCH 09/34] ThumbnailVodInfo cont --- .../main/java/com/kaltura/playkit/Player.java | 3 +- .../kaltura/playkit/PlayerDecoratorBase.java | 6 ++-- .../kaltura/playkit/PlayerEngineWrapper.java | 5 ++-- .../playkit/player/ExoPlayerWrapper.java | 2 +- .../playkit/player/PlayerController.java | 6 ++-- .../kaltura/playkit/player/PlayerEngine.java | 6 ++-- .../playkit/player/TrackSelectionHelper.java | 12 ++++---- .../player/thumbnail/ImageRangeInfo.java | 4 +-- .../player/thumbnail/ThumbnailVodInfo.java | 28 +++++++++++-------- 9 files changed, 35 insertions(+), 37 deletions(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/Player.java b/playkit/src/main/java/com/kaltura/playkit/Player.java index 88999b7c2..aea36120e 100644 --- a/playkit/src/main/java/com/kaltura/playkit/Player.java +++ b/playkit/src/main/java/com/kaltura/playkit/Player.java @@ -27,6 +27,7 @@ import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; import com.kaltura.playkit.player.VideoCodecSettings; import com.kaltura.playkit.player.AudioCodecSettings; +import com.kaltura.playkit.player.thumbnail.ThumbnailVodInfo; import com.kaltura.playkit.player.vr.VRSettings; import com.kaltura.playkit.utils.Consts; @@ -531,7 +532,7 @@ interface Settings { */ ThumbnailInfo getThumbnailInfo(long positionMS); - Map getVodThumbnailInfo(); + ThumbnailVodInfo getVodThumbnailInfo(); /** * Generic getters for playkit controllers. diff --git a/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java b/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java index 768c60793..80afb05e4 100644 --- a/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java +++ b/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java @@ -12,7 +12,6 @@ package com.kaltura.playkit; -import android.graphics.Rect; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -20,11 +19,10 @@ import com.kaltura.playkit.player.PKAspectRatioResizeMode; import com.kaltura.playkit.player.PlayerView; import com.kaltura.playkit.player.SubtitleStyleSettings; -import com.kaltura.playkit.player.thumbnail.ImageRangeInfo; import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; +import com.kaltura.playkit.player.thumbnail.ThumbnailVodInfo; import java.util.List; -import java.util.Map; public class PlayerDecoratorBase implements Player { @@ -104,7 +102,7 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { } @Override - public Map getVodThumbnailInfo() { + public ThumbnailVodInfo getVodThumbnailInfo() { return player.getVodThumbnailInfo(); } diff --git a/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java b/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java index 2c6c7323e..b703ad2ae 100644 --- a/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java @@ -13,6 +13,7 @@ import com.kaltura.playkit.player.thumbnail.ImageRangeInfo; import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; import com.kaltura.playkit.player.metadata.PKMetadata; +import com.kaltura.playkit.player.thumbnail.ThumbnailVodInfo; import java.util.List; import java.util.Map; @@ -202,10 +203,10 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { } @Override - public Map getVodThumbnailInfo() { + public ThumbnailVodInfo getVodThumbnailInfo() { return playerEngine.getVodThumbnailInfo(); } - + @Override public void setProfiler(Profiler profiler) { this.playerEngine.setProfiler(profiler); diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java index de3ef8099..3445995a4 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java @@ -1524,7 +1524,7 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { } @Override - public Map getVodThumbnailInfo() { + public ThumbnailVodInfo getVodThumbnailInfo() { log.v("getVodThumbnailInfo"); if (assertPlayerIsNotNull("getVodThumbnailInfo()")) { long playerDuration = player.getDuration(); diff --git a/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java b/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java index dd451b878..34e21310b 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java @@ -13,7 +13,6 @@ package com.kaltura.playkit.player; import android.content.Context; -import android.graphics.Rect; import android.os.Handler; import android.os.Looper; @@ -37,13 +36,12 @@ import com.kaltura.playkit.ads.AdController; import com.kaltura.playkit.ads.AdsPlayerEngineWrapper; import com.kaltura.playkit.player.metadata.URIConnectionAcquiredInfo; -import com.kaltura.playkit.player.thumbnail.ImageRangeInfo; import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; +import com.kaltura.playkit.player.thumbnail.ThumbnailVodInfo; import com.kaltura.playkit.utils.Consts; import java.io.IOException; import java.util.List; -import java.util.Map; import java.util.UUID; import static com.kaltura.playkit.utils.Consts.MILLISECONDS_MULTIPLIER; @@ -675,7 +673,7 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { } @Override - public Map getVodThumbnailInfo() { + public ThumbnailVodInfo getVodThumbnailInfo() { log.v("getVodThumbnailInfo"); if (assertPlayerIsNotNull("getVodThumbnailInfo()")) { return player.getVodThumbnailInfo(); diff --git a/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java b/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java index 4216b41a9..bf38039b1 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java @@ -12,7 +12,6 @@ package com.kaltura.playkit.player; -import android.graphics.Rect; import com.kaltura.playkit.PKController; import com.kaltura.playkit.PKError; @@ -21,13 +20,12 @@ import com.kaltura.playkit.PlayerState; import com.kaltura.playkit.player.metadata.PKMetadata; import com.kaltura.playkit.player.metadata.URIConnectionAcquiredInfo; -import com.kaltura.playkit.player.thumbnail.ImageRangeInfo; import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; +import com.kaltura.playkit.player.thumbnail.ThumbnailVodInfo; import com.kaltura.playkit.utils.Consts; import java.io.IOException; import java.util.List; -import java.util.Map; /** @@ -293,7 +291,7 @@ default void updateSurfaceAspectRatioResizeMode(PKAspectRatioResizeMode resizeMo default ThumbnailInfo getThumbnailInfo(long positionMS) { return null; } - default Map getVodThumbnailInfo() { return null; } + default ThumbnailVodInfo getVodThumbnailInfo() { return null; } interface EventListener { void onEvent(PlayerEvent.Type event); diff --git a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java index 85ad4d4a7..9d12f68ac 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java @@ -14,7 +14,6 @@ import android.content.Context; -import android.graphics.Rect; import android.text.TextUtils; import androidx.annotation.NonNull; @@ -1220,7 +1219,7 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { return new ThumbnailInfo(realImageUrl, imageX, imageY, imageWidth, imageHeight); } - public Map getVodThumbnailInfo(long mediaDurationMS) { + public ThumbnailVodInfo getVodThumbnailInfo(long mediaDurationMS) { if (imageTracks.isEmpty()) { return null; } @@ -1237,7 +1236,7 @@ public Map getVodThumbnailInfo(long mediaDurationMS) { return null; } - Map imageRangeRectMap = new LinkedHashMap<>(); + Map imageRangeThumbnailMap = new LinkedHashMap<>(); boolean isCatchup = false; final long segmentDuration = imageTrack.getSegmentDuration(); @@ -1268,7 +1267,7 @@ public Map getVodThumbnailInfo(long mediaDurationMS) { long indexValue = index; imageData = new ThumbnailVodInfo(indexValue, imageTrack, mediaDurationMS, forLoopStartNumber, isCatchup); if (imageData != null) { - imageRangeRectMap.putAll(imageData.getImageRangeRectMap()); + imageRangeThumbnailMap.putAll(imageData.getImageRangeThumbnailMap()); } rangeValueStart = rangeValueStart + segmentDuration; if (rangeValueEnd != -1) { @@ -1286,7 +1285,7 @@ public Map getVodThumbnailInfo(long mediaDurationMS) { imageData = new ThumbnailVodInfo(indexValue, imageTrack, mediaDurationMS, forLoopStartNumber, isCatchup); if (imageData != null) { - imageRangeRectMap.putAll(imageData.getImageRangeRectMap()); + imageRangeThumbnailMap.putAll(imageData.getImageRangeThumbnailMap()); } rangeValueStart = 1 + (index * segmentDuration); if (rangeValueEnd != -1) { @@ -1294,7 +1293,8 @@ public Map getVodThumbnailInfo(long mediaDurationMS) { } } } - return imageRangeRectMap; + + return new ThumbnailVodInfo(imageRangeThumbnailMap); } diff --git a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ImageRangeInfo.java b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ImageRangeInfo.java index 028f6e7ad..75adac7e6 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ImageRangeInfo.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ImageRangeInfo.java @@ -1,12 +1,10 @@ package com.kaltura.playkit.player.thumbnail; public class ImageRangeInfo { - String imagelUrl; long startPosition; long endPosition; - public ImageRangeInfo(String imagelUrl, long startPosition, long endPosition) { - this.imagelUrl = imagelUrl; + public ImageRangeInfo(long startPosition, long endPosition) { this.startPosition = startPosition; this.endPosition = endPosition; } diff --git a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java index 077979395..44620831a 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java @@ -11,12 +11,16 @@ public class ThumbnailVodInfo { - Map imageRangeRectMap; + Map imageRangeThumbnailtMap; - public Map getImageRangeRectMap() { - return imageRangeRectMap; + public Map getImageRangeThumbnailMap() { + return imageRangeThumbnailtMap; } + public ThumbnailVodInfo(Map imageRangeThumbnailMap) { + this.imageRangeThumbnailtMap = imageRangeThumbnailMap; + } + public ThumbnailVodInfo(long imageUrlIndex, ImageTrack imageTrack, long mediaDurationMS, long startNumber, boolean isCatchup) { long imageMultiplier = imageUrlIndex <= 0 ? 0 : imageUrlIndex - 1; @@ -31,7 +35,7 @@ public ThumbnailVodInfo(long imageUrlIndex, ImageTrack imageTrack, long mediaDur long singleImageDuration = (long) Math.ceil(imageTrack.getSegmentDuration() / (imageTrack.getTilesHorizontal() * imageTrack.getTilesVertical())); - imageRangeRectMap = new LinkedHashMap<>(); + imageRangeThumbnailtMap = new LinkedHashMap<>(); long rangeStart = startNumber == 1 ? 0 : startNumber; long rangeEnd = (((imageMultiplier * imageTrack.getSegmentDuration()) + singleImageDuration) - 1); long diff = 0; @@ -45,21 +49,21 @@ public ThumbnailVodInfo(long imageUrlIndex, ImageTrack imageTrack, long mediaDur for (int colIndex = 0; colIndex < imageTrack.getTilesHorizontal(); colIndex++) { //Log.d("GILAD BY POSITION THUMB", "[" + rowIndex + "," + colIndex + "] = [" + rangeStart + "," + rangeEnd + "]"); - ImageRangeInfo imageRangeInfo = new ImageRangeInfo(realImageUrl, rangeStart, rangeEnd); - Rect rect = - new Rect((colIndex * widthPerTile), - rowIndex * heightPerTile, - (colIndex * widthPerTile + widthPerTile), - rowIndex * heightPerTile + heightPerTile); + ImageRangeInfo imageRangeInfo = new ImageRangeInfo(rangeStart, rangeEnd); + ThumbnailInfo thumbnailInfo = new ThumbnailInfo(realImageUrl, colIndex * widthPerTile, rowIndex * heightPerTile, widthPerTile, heightPerTile); +// Rect rect = +// new Rect((colIndex * widthPerTile), +// rowIndex * heightPerTile, +// (colIndex * widthPerTile + widthPerTile), +// rowIndex * heightPerTile + heightPerTile); if (rangeEnd - diff > mediaDurationMS + imageTrack.getStartNumber()) { continue; } - imageRangeRectMap.put(imageRangeInfo, rect); + imageRangeThumbnailtMap.put(imageRangeInfo, thumbnailInfo); rangeStart += singleImageDuration; rangeEnd = rangeStart + diff; } } - Log.d("GILAD THUMB", "--------------------------"); } } \ No newline at end of file From dd0072a242d2181f56fa6e14bc56fa3acc270afa Mon Sep 17 00:00:00 2001 From: Gourav Saxena Date: Tue, 23 Feb 2021 18:51:12 +0530 Subject: [PATCH 10/34] - Made tile width to be calculated with Math.round --- .../com/kaltura/playkit/player/ImageTrack.java | 15 +++++++-------- .../player/thumbnail/ThumbnailVodInfo.java | 7 ++----- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ImageTrack.java b/playkit/src/main/java/com/kaltura/playkit/player/ImageTrack.java index 8cbe36b3f..55866d40e 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/ImageTrack.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/ImageTrack.java @@ -21,15 +21,14 @@ * */ public class ImageTrack extends BaseTrack { - - + private String label; private long bitrate; private String structure; private int tilesHorizontal; private int tilesVertical; - private int width; - private int height; + private float width; + private float height; private long segmentDuration; private long presentationTimeOffset; private long timeScale; @@ -40,8 +39,8 @@ public class ImageTrack extends BaseTrack { ImageTrack(String uniqueId, String label, long bitrate, - int width, - int height, + float width, + float height, int tilesHorizontal, int tilesVertical, long segmentDuration, @@ -86,11 +85,11 @@ public int getTilesVertical() { return tilesVertical; } - public int getWidth() { + public float getWidth() { return width; } - public int getHeight() { + public float getHeight() { return height; } diff --git a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java index 44620831a..38ffffa29 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java @@ -1,8 +1,5 @@ package com.kaltura.playkit.player.thumbnail; -import android.graphics.Rect; -import android.util.Log; - import com.kaltura.playkit.player.ImageTrack; import java.util.LinkedHashMap; @@ -43,8 +40,8 @@ public ThumbnailVodInfo(long imageUrlIndex, ImageTrack imageTrack, long mediaDur rangeEnd = rangeStart + rangeEnd; } diff = rangeEnd - rangeStart; - int widthPerTile = imageTrack.getWidth() / imageTrack.getTilesHorizontal(); - int heightPerTile = imageTrack.getHeight() / imageTrack.getTilesVertical(); + int widthPerTile = Math.round(imageTrack.getWidth() / imageTrack.getTilesHorizontal()); + int heightPerTile = Math.round(imageTrack.getHeight() / imageTrack.getTilesVertical()); for (int rowIndex = 0; rowIndex < imageTrack.getTilesVertical(); rowIndex++) { for (int colIndex = 0; colIndex < imageTrack.getTilesHorizontal(); colIndex++) { //Log.d("GILAD BY POSITION THUMB", "[" + rowIndex + "," + colIndex + "] = [" + rangeStart + "," + rangeEnd + "]"); From 06ef02e0c10fa385e0832b137b7e6b078a414757 Mon Sep 17 00:00:00 2001 From: Gourav Saxena Date: Tue, 23 Feb 2021 23:02:02 +0530 Subject: [PATCH 11/34] - Remove Math.floor from tile height and width - changed values from int to float --- .../playkit/player/TrackSelectionHelper.java | 14 +++++++------- .../player/thumbnail/ThumbnailInfo.java | 18 +++++++++--------- .../player/thumbnail/ThumbnailVodInfo.java | 8 +++----- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java index 9d12f68ac..fddb67aee 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java @@ -41,8 +41,8 @@ import com.kaltura.playkit.PKTrackConfig; import com.kaltura.playkit.PKVideoCodec; import com.kaltura.playkit.player.thumbnail.ImageRangeInfo; -import com.kaltura.playkit.player.thumbnail.ThumbnailVodInfo; import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; +import com.kaltura.playkit.player.thumbnail.ThumbnailVodInfo; import com.kaltura.playkit.utils.Consts; import java.util.ArrayList; @@ -1209,10 +1209,10 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { double offset = positionMS % imageTrack.getSegmentDuration(); int thumbIndex = (int) Math.floor((offset * imageTrack.getTilesHorizontal() * imageTrack.getTilesVertical()) / imageTrack.getSegmentDuration()); long seqIdx = seq + imageTrack.getStartNumber(); - int imageWidth = (int) Math.floor(imageTrack.getWidth() / imageTrack.getTilesHorizontal()); - int imageHeight = (int) Math.floor(imageTrack.getHeight() / imageTrack.getTilesVertical()); - int imageX = (int) Math.floor(thumbIndex % imageTrack.getTilesHorizontal()) * imageWidth; - int imageY = (int) Math.floor(thumbIndex / imageTrack.getTilesHorizontal()) * imageHeight; + float imageWidth = imageTrack.getWidth() / imageTrack.getTilesHorizontal(); + float imageHeight = imageTrack.getHeight() / imageTrack.getTilesVertical(); + float imageX = (float) Math.floor(thumbIndex % imageTrack.getTilesHorizontal()) * imageWidth; + float imageY = (float) Math.floor(thumbIndex / imageTrack.getTilesHorizontal()) * imageHeight; long imageRealUrlTime = ((seqIdx - 1) * imageTrack.getSegmentDuration()); String realImageUrl = imageTrack.getImageTemplateUrl().replace("$Number$", String.valueOf(seqIdx)).replace("$Time$", String.valueOf(imageRealUrlTime)); @@ -1293,7 +1293,7 @@ public ThumbnailVodInfo getVodThumbnailInfo(long mediaDurationMS) { } } } - + return new ThumbnailVodInfo(imageRangeThumbnailMap); } @@ -1861,5 +1861,5 @@ public static boolean isCodecSupported(@NonNull String codecs, @Nullable TrackTy - + } diff --git a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailInfo.java b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailInfo.java index f92248f76..289d48396 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailInfo.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailInfo.java @@ -2,12 +2,12 @@ public class ThumbnailInfo { private String url; // url of the image that contains the thumbnail slice - private int x; // x position of the thumbnail - private int y; // y position of the thumbnail - private int width; // width of the thumbnail - private int height; // height of the thumbnail + private float x; // x position of the thumbnail + private float y; // y position of the thumbnail + private float width; // width of the thumbnail + private float height; // height of the thumbnail - public ThumbnailInfo(String url, int x, int y, int width, int height) { + public ThumbnailInfo(String url, float x, float y, float width, float height) { this.url = url; this.x = x; this.y = y; @@ -19,19 +19,19 @@ public String getUrl() { return url; } - public int getX() { + public float getX() { return x; } - public int getY() { + public float getY() { return y; } - public int getWidth() { + public float getWidth() { return width; } - public int getHeight() { + public float getHeight() { return height; } } \ No newline at end of file diff --git a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java index 38ffffa29..49e8e8c74 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java @@ -17,7 +17,7 @@ public Map getImageRangeThumbnailMap() { public ThumbnailVodInfo(Map imageRangeThumbnailMap) { this.imageRangeThumbnailtMap = imageRangeThumbnailMap; } - + public ThumbnailVodInfo(long imageUrlIndex, ImageTrack imageTrack, long mediaDurationMS, long startNumber, boolean isCatchup) { long imageMultiplier = imageUrlIndex <= 0 ? 0 : imageUrlIndex - 1; @@ -40,12 +40,10 @@ public ThumbnailVodInfo(long imageUrlIndex, ImageTrack imageTrack, long mediaDur rangeEnd = rangeStart + rangeEnd; } diff = rangeEnd - rangeStart; - int widthPerTile = Math.round(imageTrack.getWidth() / imageTrack.getTilesHorizontal()); - int heightPerTile = Math.round(imageTrack.getHeight() / imageTrack.getTilesVertical()); + float widthPerTile = imageTrack.getWidth() / imageTrack.getTilesHorizontal(); + float heightPerTile = imageTrack.getHeight() / imageTrack.getTilesVertical(); for (int rowIndex = 0; rowIndex < imageTrack.getTilesVertical(); rowIndex++) { for (int colIndex = 0; colIndex < imageTrack.getTilesHorizontal(); colIndex++) { - //Log.d("GILAD BY POSITION THUMB", "[" + rowIndex + "," + colIndex + "] = [" + rangeStart + "," + rangeEnd + "]"); - ImageRangeInfo imageRangeInfo = new ImageRangeInfo(rangeStart, rangeEnd); ThumbnailInfo thumbnailInfo = new ThumbnailInfo(realImageUrl, colIndex * widthPerTile, rowIndex * heightPerTile, widthPerTile, heightPerTile); // Rect rect = From 0f3364fa98dbe55a2b374487c97a95c5b273ec9e Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Thu, 25 Feb 2021 09:19:33 +0200 Subject: [PATCH 12/34] remove import --- .../main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java | 1 - 1 file changed, 1 deletion(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java index 3445995a4..1a423c024 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java @@ -13,7 +13,6 @@ package com.kaltura.playkit.player; import android.content.Context; -import android.graphics.Rect; import android.media.MediaCodec; import android.net.Uri; import android.os.Handler; From dce63fa3865804c30447202b5ccd60982f625d64 Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Thu, 25 Feb 2021 09:21:46 +0200 Subject: [PATCH 13/34] format --- .../main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java index 1a423c024..4ee4fabdd 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java @@ -428,7 +428,8 @@ private MediaSource buildInternalExoMediaSource(MediaItem mediaItem, PKMediaSour final DataSource.Factory dataSourceFactory = getDataSourceFactory(requestParams.headers); final DataSource.Factory teedDtaSourceFactory = () -> { lastDataSink = new ByteArrayDataSink(); - return new TeeDataSource(dataSourceFactory.createDataSource(), lastDataSink); }; + return new TeeDataSource(dataSourceFactory.createDataSource(), lastDataSink); + }; MediaSource mediaSource; switch (format) { From 4b1b9c97b4b428ec5b7d5dadbfbe78ca0bc65171 Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Thu, 25 Feb 2021 10:37:53 +0200 Subject: [PATCH 14/34] fix dash manifest is not received on time --- .../playkit/player/ExoPlayerWrapper.java | 68 +++++++++++++++---- 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java index 4ee4fabdd..0cecbfdd8 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java @@ -61,12 +61,14 @@ import com.kaltura.android.exoplayer2.upstream.BandwidthMeter; import com.kaltura.android.exoplayer2.upstream.ByteArrayDataSink; import com.kaltura.android.exoplayer2.upstream.DataSource; +import com.kaltura.android.exoplayer2.upstream.DataSpec; import com.kaltura.android.exoplayer2.upstream.DefaultAllocator; import com.kaltura.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.kaltura.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.kaltura.android.exoplayer2.upstream.DefaultHttpDataSource; import com.kaltura.android.exoplayer2.upstream.HttpDataSource; import com.kaltura.android.exoplayer2.upstream.TeeDataSource; +import com.kaltura.android.exoplayer2.upstream.TransferListener; import com.kaltura.android.exoplayer2.video.CustomLoadControl; import com.kaltura.playkit.*; @@ -74,7 +76,6 @@ import com.kaltura.playkit.drm.DrmCallback; import com.kaltura.playkit.player.metadata.MetadataConverter; import com.kaltura.playkit.player.metadata.PKMetadata; -import com.kaltura.playkit.player.thumbnail.ImageRangeInfo; import com.kaltura.playkit.player.thumbnail.ThumbnailVodInfo; import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; import com.kaltura.playkit.utils.Consts; @@ -100,7 +101,8 @@ public class ExoPlayerWrapper implements PlayerEngine, Player.EventListener, MetadataOutput, BandwidthMeter.EventListener { - private ByteArrayDataSink lastDataSink; + private ByteArrayDataSink dashLastDataSink; + private String dashManifestString; public interface LoadControlStrategy { LoadControl getCustomLoadControl(); @@ -421,19 +423,58 @@ private MediaSource buildInternalExoMediaSource(MediaItem mediaItem, PKMediaSour } PKRequestParams requestParams = sourceConfig.getRequestParams(); - if (requestParams.headers == null || requestParams.headers.isEmpty()) { - //return null; - } + //if (requestParams.headers == null || requestParams.headers.isEmpty()) { + // return null; + //} final DataSource.Factory dataSourceFactory = getDataSourceFactory(requestParams.headers); - final DataSource.Factory teedDtaSourceFactory = () -> { - lastDataSink = new ByteArrayDataSink(); - return new TeeDataSource(dataSourceFactory.createDataSource(), lastDataSink); - }; + MediaSource mediaSource; switch (format) { case dash: + final DataSource.Factory teedDtaSourceFactory = () -> { + dashManifestString = null; + dashLastDataSink = new ByteArrayDataSink(); + TeeDataSource teeDataSource = new TeeDataSource(dataSourceFactory.createDataSource(), dashLastDataSink); + teeDataSource.addTransferListener(new TransferListener() { + @Override + public void onTransferInitializing(DataSource dataSource, DataSpec dataSpec, boolean b) { + + } + + @Override + public void onTransferStart(DataSource dataSource, DataSpec dataSpec, boolean b) { + + } + + @Override + public void onBytesTransferred(DataSource dataSource, DataSpec dataSpec, boolean b, int i) { + + } + + @Override + public void onTransferEnd(DataSource dataSource, DataSpec dataSpec, boolean b) { + log.d("teeDataSource onTransferEnd"); + if (dashManifestString != null) { + return; + } + if (dashLastDataSink == null) { + return; + } + + byte[] bytes = dashLastDataSink.getData(); + try { + dashManifestString = new String(bytes, "UTF-8"); + //log.d("teeDataSource manifest " + dashManifestString); + } catch (IOException e) { + log.e("teeDataSource imageTracks assemble error " + e.getMessage()); + } + } + }); + return teeDataSource; + }; + mediaSource = new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(dataSourceFactory), teedDtaSourceFactory) .setDrmSessionManager(sourceConfig.mediaSource.hasDrmParams() ? drmSessionManager : DrmSessionManager.DRM_UNSUPPORTED) @@ -882,7 +923,6 @@ public void onPositionDiscontinuity(int reason) { public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { log.d("onTracksChanged"); - String dashManifestString = ""; //if onTracksChanged happened when application went background, do not update the tracks. if (assertTrackSelectionIsNotNull("onTracksChanged()")) { @@ -890,15 +930,15 @@ public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray tra //if the track info new -> map the available tracks. and when ready, notify user about available tracks. if (shouldGetTracksInfo) { CustomDashManifest customDashManifest = null; - if (lastDataSink != null && player.getCurrentManifest() instanceof DashManifest) { - byte[] bytes = lastDataSink.getData(); + if (dashLastDataSink != null && player.getCurrentManifest() instanceof DashManifest) { + byte[] bytes = dashLastDataSink.getData(); try { - dashManifestString = new String(bytes, "UTF-8"); customDashManifest = new CustomDashManifestParser().parse(player.getMediaItemAt(0).playbackProperties.uri, dashManifestString); } catch (IOException e) { log.e("imageTracks assemble error " + e.getMessage()); } finally { - lastDataSink = null; + dashLastDataSink = null; + dashManifestString = null; } } shouldGetTracksInfo = !trackSelectionHelper.prepareTracks(trackSelections, customDashManifest); From e430d6d1181ed6b5d3e8be4c3fb8537ac84bf891 Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Thu, 25 Feb 2021 15:02:45 +0200 Subject: [PATCH 15/34] cleanup --- playkit/build.gradle | 6 ------ 1 file changed, 6 deletions(-) diff --git a/playkit/build.gradle b/playkit/build.gradle index 4f3206ae6..a279a00a5 100644 --- a/playkit/build.gradle +++ b/playkit/build.gradle @@ -53,13 +53,7 @@ dependencies { def guavaVersion = '27.1-android' api ('com.google.guava:guava:' + guavaVersion) { - // Exclude dependencies that are only used by Guava at compile time - // (but declared as runtime deps) [internal b/168188131]. - exclude group: 'com.google.code.findbugs', module: 'jsr305' exclude group: 'org.checkerframework', module: 'checker-compat-qual' - exclude group: 'com.google.errorprone', module: 'error_prone_annotations' - exclude group: 'com.google.j2objc', module: 'j2objc-annotations' - exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations' } // Tests testImplementation 'junit:junit:4.12' From 04c6994a948f7f0b0016257a21431b160314eeff Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Thu, 25 Feb 2021 15:04:59 +0200 Subject: [PATCH 16/34] remove imports --- playkit/src/main/java/com/kaltura/playkit/Player.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/Player.java b/playkit/src/main/java/com/kaltura/playkit/Player.java index aea36120e..dcbb79efe 100644 --- a/playkit/src/main/java/com/kaltura/playkit/Player.java +++ b/playkit/src/main/java/com/kaltura/playkit/Player.java @@ -12,8 +12,6 @@ package com.kaltura.playkit; -import android.graphics.Rect; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -23,7 +21,6 @@ import com.kaltura.playkit.player.PKMaxVideoSize; import com.kaltura.playkit.player.PlayerView; import com.kaltura.playkit.player.SubtitleStyleSettings; -import com.kaltura.playkit.player.thumbnail.ImageRangeInfo; import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; import com.kaltura.playkit.player.VideoCodecSettings; import com.kaltura.playkit.player.AudioCodecSettings; @@ -32,7 +29,6 @@ import com.kaltura.playkit.utils.Consts; import java.util.List; -import java.util.Map; @SuppressWarnings("unused") public interface Player { From 9b301bfb5264998bc379998e95a60e98e5c6f80b Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Thu, 25 Feb 2021 15:12:08 +0200 Subject: [PATCH 17/34] rename methid getThumbnailVodInfo --- playkit/src/main/java/com/kaltura/playkit/Player.java | 6 +++++- .../java/com/kaltura/playkit/PlayerDecoratorBase.java | 4 ++-- .../java/com/kaltura/playkit/PlayerEngineWrapper.java | 8 ++------ .../java/com/kaltura/playkit/player/ExoPlayerWrapper.java | 8 ++++---- .../java/com/kaltura/playkit/player/PlayerController.java | 8 ++++---- .../java/com/kaltura/playkit/player/PlayerEngine.java | 2 +- .../com/kaltura/playkit/player/TrackSelectionHelper.java | 2 +- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/Player.java b/playkit/src/main/java/com/kaltura/playkit/Player.java index dcbb79efe..ee8a8d97e 100644 --- a/playkit/src/main/java/com/kaltura/playkit/Player.java +++ b/playkit/src/main/java/com/kaltura/playkit/Player.java @@ -528,7 +528,11 @@ interface Settings { */ ThumbnailInfo getThumbnailInfo(long positionMS); - ThumbnailVodInfo getVodThumbnailInfo(); + /** + * get all the Information for a vod or catchup + * + */ + ThumbnailVodInfo getThumbnailVodInfo(); /** * Generic getters for playkit controllers. diff --git a/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java b/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java index 80afb05e4..540d72ac7 100644 --- a/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java +++ b/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java @@ -102,8 +102,8 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { } @Override - public ThumbnailVodInfo getVodThumbnailInfo() { - return player.getVodThumbnailInfo(); + public ThumbnailVodInfo getThumbnailVodInfo() { + return player.getThumbnailVodInfo(); } @Override diff --git a/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java b/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java index b703ad2ae..ba7450993 100644 --- a/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java @@ -1,7 +1,5 @@ package com.kaltura.playkit; -import android.graphics.Rect; - import com.kaltura.playkit.player.BaseTrack; import com.kaltura.playkit.player.PKAspectRatioResizeMode; import com.kaltura.playkit.player.PKMediaSourceConfig; @@ -10,13 +8,11 @@ import com.kaltura.playkit.player.PlayerView; import com.kaltura.playkit.player.Profiler; import com.kaltura.playkit.player.SubtitleStyleSettings; -import com.kaltura.playkit.player.thumbnail.ImageRangeInfo; import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; import com.kaltura.playkit.player.metadata.PKMetadata; import com.kaltura.playkit.player.thumbnail.ThumbnailVodInfo; import java.util.List; -import java.util.Map; public class PlayerEngineWrapper implements PlayerEngine { @@ -203,8 +199,8 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { } @Override - public ThumbnailVodInfo getVodThumbnailInfo() { - return playerEngine.getVodThumbnailInfo(); + public ThumbnailVodInfo getThumbnailVodInfo() { + return playerEngine.getThumbnailVodInfo(); } @Override diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java index 0cecbfdd8..8a0d2e822 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java @@ -1564,12 +1564,12 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { } @Override - public ThumbnailVodInfo getVodThumbnailInfo() { - log.v("getVodThumbnailInfo"); - if (assertPlayerIsNotNull("getVodThumbnailInfo()")) { + public ThumbnailVodInfo getThumbnailVodInfo() { + log.v("getThumbnailVodInfo"); + if (assertPlayerIsNotNull("getThumbnailVodInfo()")) { long playerDuration = player.getDuration(); if (playerDuration > 0) { - return trackSelectionHelper.getVodThumbnailInfo(playerDuration); + return trackSelectionHelper.getThumbnailVodInfo(playerDuration); } } return null; diff --git a/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java b/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java index 34e21310b..017594ab1 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java @@ -673,10 +673,10 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { } @Override - public ThumbnailVodInfo getVodThumbnailInfo() { - log.v("getVodThumbnailInfo"); - if (assertPlayerIsNotNull("getVodThumbnailInfo()")) { - return player.getVodThumbnailInfo(); + public ThumbnailVodInfo getThumbnailVodInfo() { + log.v("getThumbnailVodInfo"); + if (assertPlayerIsNotNull("getThumbnailVodInfo()")) { + return player.getThumbnailVodInfo(); } return null; } diff --git a/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java b/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java index bf38039b1..b7222c3c0 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java @@ -291,7 +291,7 @@ default void updateSurfaceAspectRatioResizeMode(PKAspectRatioResizeMode resizeMo default ThumbnailInfo getThumbnailInfo(long positionMS) { return null; } - default ThumbnailVodInfo getVodThumbnailInfo() { return null; } + default ThumbnailVodInfo getThumbnailVodInfo() { return null; } interface EventListener { void onEvent(PlayerEvent.Type event); diff --git a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java index fddb67aee..a33b6621a 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java @@ -1219,7 +1219,7 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { return new ThumbnailInfo(realImageUrl, imageX, imageY, imageWidth, imageHeight); } - public ThumbnailVodInfo getVodThumbnailInfo(long mediaDurationMS) { + public ThumbnailVodInfo getThumbnailVodInfo(long mediaDurationMS) { if (imageTracks.isEmpty()) { return null; } From 6d67a006be0fff0f7e9990fe65675272b095a786 Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Mon, 1 Mar 2021 08:45:18 +0200 Subject: [PATCH 18/34] remove. public api getThumbnailVodInfo --- playkit/src/main/java/com/kaltura/playkit/Player.java | 8 +------- .../java/com/kaltura/playkit/PlayerDecoratorBase.java | 5 ----- .../com/kaltura/playkit/player/PlayerController.java | 11 +---------- 3 files changed, 2 insertions(+), 22 deletions(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/Player.java b/playkit/src/main/java/com/kaltura/playkit/Player.java index ee8a8d97e..9b73b8ccf 100644 --- a/playkit/src/main/java/com/kaltura/playkit/Player.java +++ b/playkit/src/main/java/com/kaltura/playkit/Player.java @@ -527,13 +527,7 @@ interface Settings { * @param positionMS - relevant image for given player position. */ ThumbnailInfo getThumbnailInfo(long positionMS); - - /** - * get all the Information for a vod or catchup - * - */ - ThumbnailVodInfo getThumbnailVodInfo(); - + /** * Generic getters for playkit controllers. * diff --git a/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java b/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java index 540d72ac7..62e24cffd 100644 --- a/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java +++ b/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java @@ -101,11 +101,6 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { return player.getThumbnailInfo(positionMS); } - @Override - public ThumbnailVodInfo getThumbnailVodInfo() { - return player.getThumbnailVodInfo(); - } - @Override public void play() { player.play(); diff --git a/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java b/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java index 017594ab1..5e321006f 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java @@ -671,16 +671,7 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { } return null; } - - @Override - public ThumbnailVodInfo getThumbnailVodInfo() { - log.v("getThumbnailVodInfo"); - if (assertPlayerIsNotNull("getThumbnailVodInfo()")) { - return player.getThumbnailVodInfo(); - } - return null; - } - + @Override public void updateSubtitleStyle(SubtitleStyleSettings subtitleStyleSettings) { log.v("updateSubtitleStyle"); From b6aed2fba6b7bc9296ba72d6b53d34a5c78730b4 Mon Sep 17 00:00:00 2001 From: Gourav Saxena Date: Mon, 1 Mar 2021 12:33:02 +0530 Subject: [PATCH 19/34] - Firing imageTrackChanged after tracksAvailable --- .../java/com/kaltura/playkit/player/TrackSelectionHelper.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java index a33b6621a..851901c8b 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java @@ -213,6 +213,9 @@ boolean prepareTracks(TrackSelectionArray trackSelections, CustomDashManifest cu if (tracksInfoListener != null) { tracksInfoListener.onTracksInfoReady(tracksInfo); + if (!tracksInfo.getImageTracks().isEmpty()) { + tracksInfoListener.onImageTrackChanged(); + } } return true; @@ -346,7 +349,6 @@ private PKTracks buildTracks(List rawImageTracks) { if (NONE.equals(requestedChangeTrackIds[TRACK_TYPE_IMAGE])) { log.d("Image track changed to: " + requestedChangeTrackIds[TRACK_TYPE_IMAGE]); lastSelectedTrackIds[TRACK_TYPE_IMAGE] = imageTracks.get(0).getUniqueId(); - tracksInfoListener.onImageTrackChanged(); } } From 3ec54a7e26971e1544f92f64dc9d422fcbdf60d3 Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Wed, 3 Mar 2021 14:16:08 +0200 Subject: [PATCH 20/34] Update PlayerDecoratorBase.java --- .../src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java | 1 - 1 file changed, 1 deletion(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java b/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java index 62e24cffd..23a473a86 100644 --- a/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java +++ b/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java @@ -12,7 +12,6 @@ package com.kaltura.playkit; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; From c5bf8944a513d24b47ff850ae622a65c8a9cb5cd Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Wed, 3 Mar 2021 14:18:56 +0200 Subject: [PATCH 21/34] Update ExoPlayerWrapper.java --- .../main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java | 1 - 1 file changed, 1 deletion(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java index 8a0d2e822..0351eda46 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java @@ -429,7 +429,6 @@ private MediaSource buildInternalExoMediaSource(MediaItem mediaItem, PKMediaSour final DataSource.Factory dataSourceFactory = getDataSourceFactory(requestParams.headers); - MediaSource mediaSource; switch (format) { case dash: From 30e9af08183cb8fc39b02e14cb7070881b66c08a Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Wed, 3 Mar 2021 17:06:37 +0200 Subject: [PATCH 22/34] clean up --- .../playkit/player/ExoPlayerWrapper.java | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java index 0351eda46..f1458f843 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java @@ -21,6 +21,7 @@ import androidx.annotation.NonNull; +import com.google.common.base.Charsets; import com.kaltura.android.exoplayer2.C; import com.kaltura.android.exoplayer2.DefaultLoadControl; import com.kaltura.android.exoplayer2.DefaultRenderersFactory; @@ -91,6 +92,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.TimeUnit; import okhttp3.OkHttpClient; @@ -308,6 +310,7 @@ private void preparePlayer(@NonNull PKMediaSourceConfig sourceConfig) { if (!isLocalMediaItem(sourceConfig) && !isLocalMediaSource(sourceConfig)) { mediaSource = buildInternalExoMediaSource(mediaItem, sourceConfig); } + if (mediaItem != null) { profiler.onPrepareStarted(sourceConfig); if (mediaSource == null) { @@ -352,6 +355,10 @@ private void sendPrepareSourceError(@NonNull PKMediaSourceConfig sourceConfig) { private MediaItem buildExoMediaItem(PKMediaSourceConfig sourceConfig) { List externalSubtitleList = null; + if (sourceConfig == null || sourceConfig.mediaSource == null) { + return null; + } + if (sourceConfig.getExternalSubtitleList() != null) { externalSubtitleList = sourceConfig.getExternalSubtitleList().size() > 0 ? sourceConfig.getExternalSubtitleList() : null; @@ -379,9 +386,8 @@ private MediaItem buildExoMediaItem(PKMediaSourceConfig sourceConfig) { mediaItem = buildInternalExoMediaItem(sourceConfig, externalSubtitleList); } - MediaItem.DrmConfiguration drmConfiguration = mediaItem.playbackProperties.drmConfiguration; + MediaItem.DrmConfiguration drmConfiguration = Objects.requireNonNull(mediaItem.playbackProperties).drmConfiguration; if (!(sourceConfig.mediaSource instanceof LocalAssetsManager.LocalMediaSource) && - drmConfiguration != null && drmConfiguration.licenseUri != null && !TextUtils.isEmpty(drmConfiguration.licenseUri.toString())) { @@ -423,10 +429,6 @@ private MediaSource buildInternalExoMediaSource(MediaItem mediaItem, PKMediaSour } PKRequestParams requestParams = sourceConfig.getRequestParams(); - //if (requestParams.headers == null || requestParams.headers.isEmpty()) { - // return null; - //} - final DataSource.Factory dataSourceFactory = getDataSourceFactory(requestParams.headers); MediaSource mediaSource; @@ -438,22 +440,22 @@ private MediaSource buildInternalExoMediaSource(MediaItem mediaItem, PKMediaSour TeeDataSource teeDataSource = new TeeDataSource(dataSourceFactory.createDataSource(), dashLastDataSink); teeDataSource.addTransferListener(new TransferListener() { @Override - public void onTransferInitializing(DataSource dataSource, DataSpec dataSpec, boolean b) { + public void onTransferInitializing(@NonNull DataSource dataSource, @NonNull DataSpec dataSpec, boolean b) { } @Override - public void onTransferStart(DataSource dataSource, DataSpec dataSpec, boolean b) { + public void onTransferStart(@NonNull DataSource dataSource, @NonNull DataSpec dataSpec, boolean b) { } @Override - public void onBytesTransferred(DataSource dataSource, DataSpec dataSpec, boolean b, int i) { + public void onBytesTransferred(@NonNull DataSource dataSource, @NonNull DataSpec dataSpec, boolean b, int i) { } @Override - public void onTransferEnd(DataSource dataSource, DataSpec dataSpec, boolean b) { + public void onTransferEnd(@NonNull DataSource dataSource, @NonNull DataSpec dataSpec, boolean b) { log.d("teeDataSource onTransferEnd"); if (dashManifestString != null) { return; @@ -463,12 +465,8 @@ public void onTransferEnd(DataSource dataSource, DataSpec dataSpec, boolean b) { } byte[] bytes = dashLastDataSink.getData(); - try { - dashManifestString = new String(bytes, "UTF-8"); - //log.d("teeDataSource manifest " + dashManifestString); - } catch (IOException e) { - log.e("teeDataSource imageTracks assemble error " + e.getMessage()); - } + dashManifestString = new String(bytes, Charsets.UTF_8); + //log.d("teeDataSource manifest " + dashManifestString); } }); return teeDataSource; @@ -821,7 +819,12 @@ public void onPlayerError(ExoPlaybackException error) { log.d("onPlayerError BehindLiveWindowException received, re-preparing player"); MediaItem mediaItem = buildExoMediaItem(sourceConfig); if (mediaItem != null) { - player.setMediaItems(Collections.singletonList(mediaItem), 0, C.TIME_UNSET); + PKMediaFormat format = sourceConfig.mediaSource.getMediaFormat(); + if (format == null) { + player.setMediaItems(Collections.singletonList(mediaItem), 0, C.TIME_UNSET); + } else { + player.setMediaSources(Collections.singletonList(buildInternalExoMediaSource(mediaItem, sourceConfig)), 0, playerPosition == TIME_UNSET ? 0 : playerPosition); + } player.prepare(); } else { sendPrepareSourceError(sourceConfig); From 00345db71de7dcc38a3bcbee1cc8880815f38aec Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Wed, 3 Mar 2021 17:16:58 +0200 Subject: [PATCH 23/34] fix warning --- .../kaltura/playkit/player/ExoPlayerWrapper.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java index f1458f843..ae2bf074d 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java @@ -386,13 +386,15 @@ private MediaItem buildExoMediaItem(PKMediaSourceConfig sourceConfig) { mediaItem = buildInternalExoMediaItem(sourceConfig, externalSubtitleList); } - MediaItem.DrmConfiguration drmConfiguration = Objects.requireNonNull(mediaItem.playbackProperties).drmConfiguration; - if (!(sourceConfig.mediaSource instanceof LocalAssetsManager.LocalMediaSource) && - drmConfiguration != null && - drmConfiguration.licenseUri != null && - !TextUtils.isEmpty(drmConfiguration.licenseUri.toString())) { - drmSessionManager = getDeferredDRMSessionManager(); - drmSessionManager.setLicenseUrl(drmConfiguration.licenseUri.toString()); + if (mediaItem.playbackProperties != null) { + MediaItem.DrmConfiguration drmConfiguration = mediaItem.playbackProperties.drmConfiguration; + if (!(sourceConfig.mediaSource instanceof LocalAssetsManager.LocalMediaSource) && + drmConfiguration != null && + drmConfiguration.licenseUri != null && + !TextUtils.isEmpty(drmConfiguration.licenseUri.toString())) { + drmSessionManager = getDeferredDRMSessionManager(); + drmSessionManager.setLicenseUrl(drmConfiguration.licenseUri.toString()); + } } if (drmSessionManager != null) { From f5e8ecd7d768326980c1e8771714351a8408352f Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Wed, 3 Mar 2021 17:18:56 +0200 Subject: [PATCH 24/34] Update PlayerEngineWrapper.java --- .../src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java b/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java index ba7450993..442bed185 100644 --- a/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java @@ -8,8 +8,8 @@ import com.kaltura.playkit.player.PlayerView; import com.kaltura.playkit.player.Profiler; import com.kaltura.playkit.player.SubtitleStyleSettings; -import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; import com.kaltura.playkit.player.metadata.PKMetadata; +import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; import com.kaltura.playkit.player.thumbnail.ThumbnailVodInfo; import java.util.List; From 5f5b9f922b831a28f8fcdd7659c64a413f41748e Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Wed, 3 Mar 2021 17:19:31 +0200 Subject: [PATCH 25/34] Update Player.java --- playkit/src/main/java/com/kaltura/playkit/Player.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/Player.java b/playkit/src/main/java/com/kaltura/playkit/Player.java index 9b73b8ccf..36103f6ec 100644 --- a/playkit/src/main/java/com/kaltura/playkit/Player.java +++ b/playkit/src/main/java/com/kaltura/playkit/Player.java @@ -21,9 +21,9 @@ import com.kaltura.playkit.player.PKMaxVideoSize; import com.kaltura.playkit.player.PlayerView; import com.kaltura.playkit.player.SubtitleStyleSettings; -import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; import com.kaltura.playkit.player.VideoCodecSettings; import com.kaltura.playkit.player.AudioCodecSettings; +import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; import com.kaltura.playkit.player.thumbnail.ThumbnailVodInfo; import com.kaltura.playkit.player.vr.VRSettings; import com.kaltura.playkit.utils.Consts; From fa76c991d5fd1ff2fa503d17e7bbe3ed9c49eb75 Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Wed, 3 Mar 2021 17:21:04 +0200 Subject: [PATCH 26/34] remove import --- playkit/src/main/java/com/kaltura/playkit/Player.java | 1 - 1 file changed, 1 deletion(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/Player.java b/playkit/src/main/java/com/kaltura/playkit/Player.java index 36103f6ec..b29544df4 100644 --- a/playkit/src/main/java/com/kaltura/playkit/Player.java +++ b/playkit/src/main/java/com/kaltura/playkit/Player.java @@ -24,7 +24,6 @@ import com.kaltura.playkit.player.VideoCodecSettings; import com.kaltura.playkit.player.AudioCodecSettings; import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; -import com.kaltura.playkit.player.thumbnail.ThumbnailVodInfo; import com.kaltura.playkit.player.vr.VRSettings; import com.kaltura.playkit.utils.Consts; From fec94ecaf88a56a81a45ee20cb22e066094eec8f Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Wed, 3 Mar 2021 17:22:10 +0200 Subject: [PATCH 27/34] remove import --- .../src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java | 1 - 1 file changed, 1 deletion(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java b/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java index 23a473a86..068bdfe31 100644 --- a/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java +++ b/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java @@ -19,7 +19,6 @@ import com.kaltura.playkit.player.PlayerView; import com.kaltura.playkit.player.SubtitleStyleSettings; import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; -import com.kaltura.playkit.player.thumbnail.ThumbnailVodInfo; import java.util.List; From bcc864a8c8a98d3ed0518ef512187a0ca942b077 Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Wed, 3 Mar 2021 17:27:12 +0200 Subject: [PATCH 28/34] fix imports --- .../main/java/com/kaltura/playkit/player/PlayerController.java | 1 - 1 file changed, 1 deletion(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java b/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java index 5e321006f..977854c70 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java @@ -37,7 +37,6 @@ import com.kaltura.playkit.ads.AdsPlayerEngineWrapper; import com.kaltura.playkit.player.metadata.URIConnectionAcquiredInfo; import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; -import com.kaltura.playkit.player.thumbnail.ThumbnailVodInfo; import com.kaltura.playkit.utils.Consts; import java.io.IOException; From c1bb425c344baea35d16b65c26a20de20fd9db2b Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Wed, 3 Mar 2021 17:34:55 +0200 Subject: [PATCH 29/34] cleanup --- .../com/kaltura/playkit/player/PlayerEngine.java | 2 -- .../playkit/player/thumbnail/ImageRangeInfo.java | 13 +++++++++++-- .../player/thumbnail/ThumbnailDimensions.java | 14 +++++++++++--- .../playkit/player/thumbnail/ThumbnailInfo.java | 12 ++++++------ .../playkit/player/thumbnail/ThumbnailVodInfo.java | 7 +------ 5 files changed, 29 insertions(+), 19 deletions(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java b/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java index b7222c3c0..912c1467e 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java @@ -12,7 +12,6 @@ package com.kaltura.playkit.player; - import com.kaltura.playkit.PKController; import com.kaltura.playkit.PKError; import com.kaltura.playkit.PlaybackInfo; @@ -27,7 +26,6 @@ import java.io.IOException; import java.util.List; - /** * Interface that connect between {@link PlayerController} and actual player engine * {@link ExoPlayerWrapper} or MediaPlayerWrapper. Depends on the type of media that diff --git a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ImageRangeInfo.java b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ImageRangeInfo.java index 75adac7e6..fd4811068 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ImageRangeInfo.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ImageRangeInfo.java @@ -1,11 +1,20 @@ package com.kaltura.playkit.player.thumbnail; public class ImageRangeInfo { - long startPosition; - long endPosition; + private final long startPosition; + private final long endPosition; public ImageRangeInfo(long startPosition, long endPosition) { this.startPosition = startPosition; this.endPosition = endPosition; } + + public long getStartPosition() { + return startPosition; + } + + public long getEndPosition() { + return endPosition; + } } + diff --git a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailDimensions.java b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailDimensions.java index 23b2e37cb..883453887 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailDimensions.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailDimensions.java @@ -1,11 +1,19 @@ package com.kaltura.playkit.player.thumbnail; public class ThumbnailDimensions { - int width; - int height; + private final int width; + private final int height; public ThumbnailDimensions(int width, int height) { this.width = width; this.height = height; } -} \ No newline at end of file + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } +} diff --git a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailInfo.java b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailInfo.java index 289d48396..efa306efc 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailInfo.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailInfo.java @@ -1,11 +1,11 @@ package com.kaltura.playkit.player.thumbnail; public class ThumbnailInfo { - private String url; // url of the image that contains the thumbnail slice - private float x; // x position of the thumbnail - private float y; // y position of the thumbnail - private float width; // width of the thumbnail - private float height; // height of the thumbnail + private final String url; // url of the image that contains the thumbnail slice + private final float x; // x position of the thumbnail + private final float y; // y position of the thumbnail + private final float width; // width of the thumbnail + private final float height; // height of the thumbnail public ThumbnailInfo(String url, float x, float y, float width, float height) { this.url = url; @@ -34,4 +34,4 @@ public float getWidth() { public float getHeight() { return height; } -} \ No newline at end of file +} diff --git a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java index 49e8e8c74..90c88064d 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java @@ -46,11 +46,6 @@ public ThumbnailVodInfo(long imageUrlIndex, ImageTrack imageTrack, long mediaDur for (int colIndex = 0; colIndex < imageTrack.getTilesHorizontal(); colIndex++) { ImageRangeInfo imageRangeInfo = new ImageRangeInfo(rangeStart, rangeEnd); ThumbnailInfo thumbnailInfo = new ThumbnailInfo(realImageUrl, colIndex * widthPerTile, rowIndex * heightPerTile, widthPerTile, heightPerTile); -// Rect rect = -// new Rect((colIndex * widthPerTile), -// rowIndex * heightPerTile, -// (colIndex * widthPerTile + widthPerTile), -// rowIndex * heightPerTile + heightPerTile); if (rangeEnd - diff > mediaDurationMS + imageTrack.getStartNumber()) { continue; @@ -61,4 +56,4 @@ public ThumbnailVodInfo(long imageUrlIndex, ImageTrack imageTrack, long mediaDur } } } -} \ No newline at end of file +} From c69aae5fb214ececc2063b374588da7fb3a4e7d9 Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Sun, 7 Mar 2021 12:53:13 +0200 Subject: [PATCH 30/34] cleanup --- .../main/java/com/kaltura/playkit/player/ImageTrack.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ImageTrack.java b/playkit/src/main/java/com/kaltura/playkit/player/ImageTrack.java index 55866d40e..5994f32fd 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/ImageTrack.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/ImageTrack.java @@ -24,7 +24,6 @@ public class ImageTrack extends BaseTrack { private String label; private long bitrate; - private String structure; private int tilesHorizontal; private int tilesVertical; private float width; @@ -72,11 +71,7 @@ public String getLabel() { public long getBitrate() { return bitrate; } - - public String getStructure() { - return structure; - } - + public int getTilesHorizontal() { return tilesHorizontal; } From fa54cd221cb8a94088eed2932a9188010bc7b495 Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Sun, 7 Mar 2021 20:35:48 +0200 Subject: [PATCH 31/34] remove public modifier --- .../com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java index 90c88064d..f8751af7a 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java @@ -6,7 +6,7 @@ import java.util.Map; -public class ThumbnailVodInfo { +class ThumbnailVodInfo { Map imageRangeThumbnailtMap; From c87f3d8c12ee905d189b33f2cb526cb2b6f358fc Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Mon, 8 Mar 2021 13:18:58 +0200 Subject: [PATCH 32/34] cleanup --- .../kaltura/playkit/PlayerEngineWrapper.java | 6 -- .../playkit/player/ExoPlayerWrapper.java | 14 ---- .../kaltura/playkit/player/PlayerEngine.java | 3 - .../playkit/player/TrackSelectionHelper.java | 81 ------------------- .../player/thumbnail/ThumbnailVodInfo.java | 2 +- 5 files changed, 1 insertion(+), 105 deletions(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java b/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java index 442bed185..4de937a03 100644 --- a/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/PlayerEngineWrapper.java @@ -10,7 +10,6 @@ import com.kaltura.playkit.player.SubtitleStyleSettings; import com.kaltura.playkit.player.metadata.PKMetadata; import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; -import com.kaltura.playkit.player.thumbnail.ThumbnailVodInfo; import java.util.List; @@ -198,11 +197,6 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { return playerEngine.getThumbnailInfo(positionMS); } - @Override - public ThumbnailVodInfo getThumbnailVodInfo() { - return playerEngine.getThumbnailVodInfo(); - } - @Override public void setProfiler(Profiler profiler) { this.playerEngine.setProfiler(profiler); diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java index 5bcc6332f..0696402d6 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java @@ -77,7 +77,6 @@ import com.kaltura.playkit.drm.DrmCallback; import com.kaltura.playkit.player.metadata.MetadataConverter; import com.kaltura.playkit.player.metadata.PKMetadata; -import com.kaltura.playkit.player.thumbnail.ThumbnailVodInfo; import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; import com.kaltura.playkit.utils.Consts; import com.kaltura.playkit.utils.NativeCookieJarBridge; @@ -1567,19 +1566,6 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { return null; } - @Override - public ThumbnailVodInfo getThumbnailVodInfo() { - log.v("getThumbnailVodInfo"); - if (assertPlayerIsNotNull("getThumbnailVodInfo()")) { - long playerDuration = player.getDuration(); - if (playerDuration > 0) { - return trackSelectionHelper.getThumbnailVodInfo(playerDuration); - } - } - return null; - } - - private void closeProfilerSession() { profiler.onSessionFinished(); } diff --git a/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java b/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java index 912c1467e..d7dbd9932 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/PlayerEngine.java @@ -20,7 +20,6 @@ import com.kaltura.playkit.player.metadata.PKMetadata; import com.kaltura.playkit.player.metadata.URIConnectionAcquiredInfo; import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; -import com.kaltura.playkit.player.thumbnail.ThumbnailVodInfo; import com.kaltura.playkit.utils.Consts; import java.io.IOException; @@ -289,8 +288,6 @@ default void updateSurfaceAspectRatioResizeMode(PKAspectRatioResizeMode resizeMo default ThumbnailInfo getThumbnailInfo(long positionMS) { return null; } - default ThumbnailVodInfo getThumbnailVodInfo() { return null; } - interface EventListener { void onEvent(PlayerEvent.Type event); } diff --git a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java index 851901c8b..e7547138c 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java @@ -42,7 +42,6 @@ import com.kaltura.playkit.PKVideoCodec; import com.kaltura.playkit.player.thumbnail.ImageRangeInfo; import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; -import com.kaltura.playkit.player.thumbnail.ThumbnailVodInfo; import com.kaltura.playkit.utils.Consts; import java.util.ArrayList; @@ -1221,86 +1220,6 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { return new ThumbnailInfo(realImageUrl, imageX, imageY, imageWidth, imageHeight); } - public ThumbnailVodInfo getThumbnailVodInfo(long mediaDurationMS) { - if (imageTracks.isEmpty()) { - return null; - } - - ImageTrack imageTrack = null; - for (int index = 0; index < imageTracks.size() ; index++) { - if (imageTracks.get(index).getUniqueId().equals(lastSelectedTrackIds[TRACK_TYPE_IMAGE])) { - imageTrack = imageTracks.get(index); - break; - } - } - - if (imageTrack == null) { - return null; - } - - Map imageRangeThumbnailMap = new LinkedHashMap<>(); - - boolean isCatchup = false; - final long segmentDuration = imageTrack.getSegmentDuration(); - - int maxIndex = (int) Math.ceil((mediaDurationMS * 1.0) / segmentDuration); - if (imageTrack.getTilesVertical() == 1 && imageTrack.getTilesHorizontal() == 1) { - maxIndex = (int) Math.ceil((maxIndex) * ((segmentDuration / 1000.0) / 1)); - } - - if (maxIndex < imageTrack.getStartNumber()) { - isCatchup = true; - } - - ThumbnailVodInfo imageData = null; - long forLoopStartNumber; - long forLoopEndNumber; - if (isCatchup) { - long rangeValueStart = imageTrack.getStartNumber(); - long rangeValueEnd = imageTrack.getEndNumber() == -1 ? segmentDuration : imageTrack.getEndNumber(); - - - maxIndex += imageTrack.getStartNumber(); - rangeValueEnd += imageTrack.getStartNumber(); - - for (long index = imageTrack.getStartNumber(); index <= maxIndex; index++) { - - forLoopStartNumber = rangeValueStart; - long indexValue = index; - imageData = new ThumbnailVodInfo(indexValue, imageTrack, mediaDurationMS, forLoopStartNumber, isCatchup); - if (imageData != null) { - imageRangeThumbnailMap.putAll(imageData.getImageRangeThumbnailMap()); - } - rangeValueStart = rangeValueStart + segmentDuration; - if (rangeValueEnd != -1) { - rangeValueEnd += segmentDuration; - } - } - } else { - long rangeValueStart = imageTrack.getStartNumber(); - long rangeValueEnd = imageTrack.getEndNumber() == -1 ? segmentDuration : imageTrack.getEndNumber(); - - for (long index = imageTrack.getStartNumber(); index <= maxIndex; index++) { - - forLoopStartNumber = rangeValueStart; - long indexValue = index; - - imageData = new ThumbnailVodInfo(indexValue, imageTrack, mediaDurationMS, forLoopStartNumber, isCatchup); - if (imageData != null) { - imageRangeThumbnailMap.putAll(imageData.getImageRangeThumbnailMap()); - } - rangeValueStart = 1 + (index * segmentDuration); - if (rangeValueEnd != -1) { - rangeValueEnd += segmentDuration; - } - } - } - - return new ThumbnailVodInfo(imageRangeThumbnailMap); - } - - - /** * Checks if adaptive track for the specified group was created. * diff --git a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java index f8751af7a..58054822f 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java @@ -35,7 +35,7 @@ public ThumbnailVodInfo(long imageUrlIndex, ImageTrack imageTrack, long mediaDur imageRangeThumbnailtMap = new LinkedHashMap<>(); long rangeStart = startNumber == 1 ? 0 : startNumber; long rangeEnd = (((imageMultiplier * imageTrack.getSegmentDuration()) + singleImageDuration) - 1); - long diff = 0; + long diff; if (rangeStart > rangeEnd) { rangeEnd = rangeStart + rangeEnd; } From a755a8d7fc5d2a620350bda40a9227235e551274 Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Mon, 8 Mar 2021 15:59:44 +0200 Subject: [PATCH 33/34] getThumbnailInfo - position is optional now if not passed will be using current position --- playkit/src/main/java/com/kaltura/playkit/Player.java | 5 +++-- .../java/com/kaltura/playkit/PlayerDecoratorBase.java | 2 +- .../java/com/kaltura/playkit/player/PlayerController.java | 8 ++++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/playkit/src/main/java/com/kaltura/playkit/Player.java b/playkit/src/main/java/com/kaltura/playkit/Player.java index b29544df4..3cbcc3146 100644 --- a/playkit/src/main/java/com/kaltura/playkit/Player.java +++ b/playkit/src/main/java/com/kaltura/playkit/Player.java @@ -522,10 +522,11 @@ interface Settings { /** * get the Information for a thumbnailImage by position + * if positionMS is not passed current position will be used * - * @param positionMS - relevant image for given player position. + * @param positionMS - relevant image for given player position. (optional) */ - ThumbnailInfo getThumbnailInfo(long positionMS); + ThumbnailInfo getThumbnailInfo(long ... positionMS); /** * Generic getters for playkit controllers. diff --git a/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java b/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java index 068bdfe31..54fc975cf 100644 --- a/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java +++ b/playkit/src/main/java/com/kaltura/playkit/PlayerDecoratorBase.java @@ -95,7 +95,7 @@ public float getPlaybackRate() { } @Override - public ThumbnailInfo getThumbnailInfo(long positionMS) { + public ThumbnailInfo getThumbnailInfo(long ... positionMS) { return player.getThumbnailInfo(positionMS); } diff --git a/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java b/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java index 977854c70..82e3b4469 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/PlayerController.java @@ -663,10 +663,14 @@ public float getPlaybackRate() { } @Override - public ThumbnailInfo getThumbnailInfo(long positionMS) { + public ThumbnailInfo getThumbnailInfo(long ... positionMS) { log.v("getThumbnailInfo"); if (assertPlayerIsNotNull("getThumbnailInfo()")) { - return player.getThumbnailInfo(positionMS); + if (positionMS.length > 0) { + return player.getThumbnailInfo(positionMS[0]); + } else { + return player.getThumbnailInfo(player.getCurrentPosition()); + } } return null; } From 622f3ec3a69641c3410dc03cff2bee0d06ade674 Mon Sep 17 00:00:00 2001 From: Gilad Nadav Date: Tue, 16 Mar 2021 18:41:54 +0200 Subject: [PATCH 34/34] code review changes Use DashImageTrack --- .../playkit/player/DashImageTrack.java | 64 ++++++++++++++++ .../kaltura/playkit/player/ImageTrack.java | 74 ++++++------------- .../playkit/player/TrackSelectionHelper.java | 30 ++++---- .../player/thumbnail/ThumbnailVodInfo.java | 23 +++--- 4 files changed, 111 insertions(+), 80 deletions(-) create mode 100644 playkit/src/main/java/com/kaltura/playkit/player/DashImageTrack.java diff --git a/playkit/src/main/java/com/kaltura/playkit/player/DashImageTrack.java b/playkit/src/main/java/com/kaltura/playkit/player/DashImageTrack.java new file mode 100644 index 000000000..e8f024bdb --- /dev/null +++ b/playkit/src/main/java/com/kaltura/playkit/player/DashImageTrack.java @@ -0,0 +1,64 @@ +/* + * ============================================================================ + * Copyright (C) 2017 Kaltura Inc. + * + * Licensed under the AGPLv3 license, unless a different license for a + * particular library is specified in the applicable library path. + * + * You may obtain a copy of the License at + * https://www.gnu.org/licenses/agpl-3.0.html + * ============================================================================ + */ + +package com.kaltura.playkit.player; + +/** + * Image track data holder. + * + */ +public class DashImageTrack extends ImageTrack { + + private long presentationTimeOffset; + private long timeScale; + private long startNumber; + private long endNumber; + + DashImageTrack(String uniqueId, + String label, + long bitrate, + float width, + float height, + int cols, + int rows, + long duration, + String url, + long presentationTimeOffset, + long timeScale, + long startNumber, + long endtNumber + ) { + super(uniqueId, label, bitrate, width, height, cols, rows, duration, url); + + this.presentationTimeOffset = presentationTimeOffset; + this.timeScale = timeScale; + this.startNumber = startNumber; + this.endNumber = endtNumber; + } + + public long getPresentationTimeOffset() { + return presentationTimeOffset; + } + + public long getTimeScale() { + return timeScale; + } + + public long getStartNumber() { + return startNumber; + } + + public long getEndNumber() { + return endNumber; + } + +} diff --git a/playkit/src/main/java/com/kaltura/playkit/player/ImageTrack.java b/playkit/src/main/java/com/kaltura/playkit/player/ImageTrack.java index 5994f32fd..d42f74fe7 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/ImageTrack.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/ImageTrack.java @@ -12,56 +12,40 @@ package com.kaltura.playkit.player; -import androidx.annotation.Nullable; - -import com.kaltura.playkit.PKAudioCodec; - /** * Image track data holder. * */ public class ImageTrack extends BaseTrack { - + private String label; private long bitrate; - private int tilesHorizontal; - private int tilesVertical; private float width; private float height; - private long segmentDuration; - private long presentationTimeOffset; - private long timeScale; - private long startNumber; - private long endNumber; - private String imageTemplateUrl; + private int cols; + private int rows; + private long duration; + private String url; ImageTrack(String uniqueId, String label, long bitrate, float width, float height, - int tilesHorizontal, - int tilesVertical, - long segmentDuration, - long startNumber, - long endtNumber, - long presentationTimeOffset, - long timeScale, - String imageTemplateUrl + int cols, + int rows, + long duration, + String url ) { super(uniqueId, 0, false); this.label = label; this.bitrate = bitrate; this.width = width; this.height = height; - this.tilesHorizontal = tilesHorizontal; - this.tilesVertical = tilesVertical; - this.segmentDuration = segmentDuration; - this.startNumber = startNumber; - this.endNumber = endtNumber; - this.presentationTimeOffset = presentationTimeOffset; - this.timeScale = timeScale; - this.imageTemplateUrl = imageTemplateUrl; + this.cols = cols; + this.rows = rows; + this.duration = duration; + this.url = url; } public String getLabel() { @@ -72,12 +56,12 @@ public long getBitrate() { return bitrate; } - public int getTilesHorizontal() { - return tilesHorizontal; + public int getCols() { + return cols; } - public int getTilesVertical() { - return tilesVertical; + public int getRows() { + return rows; } public float getWidth() { @@ -88,27 +72,11 @@ public float getHeight() { return height; } - public long getSegmentDuration() { - return segmentDuration; - } - - public long getPresentationTimeOffset() { - return presentationTimeOffset; - } - - public long getTimeScale() { - return timeScale; - } - - public long getStartNumber() { - return startNumber; - } - - public long getEndNumber() { - return endNumber; + public long getDuration() { + return duration; } - public String getImageTemplateUrl() { - return imageTemplateUrl; + public String getUrl() { + return url; } } diff --git a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java index b09e20f1c..7bf1d8c12 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java @@ -40,7 +40,6 @@ import com.kaltura.playkit.PKSubtitlePreference; import com.kaltura.playkit.PKTrackConfig; import com.kaltura.playkit.PKVideoCodec; -import com.kaltura.playkit.player.thumbnail.ImageRangeInfo; import com.kaltura.playkit.player.thumbnail.ThumbnailInfo; import com.kaltura.playkit.utils.Consts; @@ -49,7 +48,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.Iterator; -import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -330,7 +328,7 @@ private PKTracks buildTracks(List rawImageTracks) { CustomFormat imageFormat = rawImageTracks.get(trackIndex); CustomFormat.FormatThumbnailInfo formatThumbnailInfo = imageFormat.formatThumbnailInfo; String uniqueId = getUniqueId(TRACK_TYPE_IMAGE, TRACK_TYPE_IMAGE, trackIndex); - imageTracks.add(trackIndex, new ImageTrack(uniqueId, + imageTracks.add(trackIndex, new DashImageTrack(uniqueId, imageFormat.id, imageFormat.bitrate, imageFormat.width, @@ -338,11 +336,11 @@ private PKTracks buildTracks(List rawImageTracks) { formatThumbnailInfo.tilesHorizontal, formatThumbnailInfo.tilesVertical, formatThumbnailInfo.segmentDuration * Consts.MILLISECONDS_MULTIPLIER, - formatThumbnailInfo.startNumber, - formatThumbnailInfo.endNumber, + formatThumbnailInfo.imageTemplateUrl, formatThumbnailInfo.presentationTimeOffset, formatThumbnailInfo.timeScale, - formatThumbnailInfo.imageTemplateUrl + formatThumbnailInfo.startNumber, + formatThumbnailInfo.endNumber )); } @@ -1214,17 +1212,17 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { return null; } - long seq = (long)Math.floor(positionMS / imageTrack.getSegmentDuration()); - double offset = positionMS % imageTrack.getSegmentDuration(); - int thumbIndex = (int) Math.floor((offset * imageTrack.getTilesHorizontal() * imageTrack.getTilesVertical()) / imageTrack.getSegmentDuration()); - long seqIdx = seq + imageTrack.getStartNumber(); - float imageWidth = imageTrack.getWidth() / imageTrack.getTilesHorizontal(); - float imageHeight = imageTrack.getHeight() / imageTrack.getTilesVertical(); - float imageX = (float) Math.floor(thumbIndex % imageTrack.getTilesHorizontal()) * imageWidth; - float imageY = (float) Math.floor(thumbIndex / imageTrack.getTilesHorizontal()) * imageHeight; + long seq = (long)Math.floor(positionMS / imageTrack.getDuration()); + double offset = positionMS % imageTrack.getDuration(); + int thumbIndex = (int) Math.floor((offset * imageTrack.getCols() * imageTrack.getRows()) / imageTrack.getDuration()); + long seqIdx = seq + ((DashImageTrack)imageTrack).getStartNumber(); + float imageWidth = imageTrack.getWidth() / imageTrack.getCols(); + float imageHeight = imageTrack.getHeight() / imageTrack.getRows(); + float imageX = (float) Math.floor(thumbIndex % imageTrack.getCols()) * imageWidth; + float imageY = (float) Math.floor(thumbIndex / imageTrack.getCols()) * imageHeight; - long imageRealUrlTime = ((seqIdx - 1) * imageTrack.getSegmentDuration()); - String realImageUrl = imageTrack.getImageTemplateUrl().replace("$Number$", String.valueOf(seqIdx)).replace("$Time$", String.valueOf(imageRealUrlTime)); + long imageRealUrlTime = ((seqIdx - 1) * imageTrack.getDuration()); + String realImageUrl = imageTrack.getUrl().replace("$Number$", String.valueOf(seqIdx)).replace("$Time$", String.valueOf(imageRealUrlTime)); return new ThumbnailInfo(realImageUrl, imageX, imageY, imageWidth, imageHeight); } diff --git a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java index 58054822f..afc1043da 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/thumbnail/ThumbnailVodInfo.java @@ -1,5 +1,6 @@ package com.kaltura.playkit.player.thumbnail; +import com.kaltura.playkit.player.DashImageTrack; import com.kaltura.playkit.player.ImageTrack; import java.util.LinkedHashMap; @@ -21,33 +22,33 @@ public ThumbnailVodInfo(Map imageRangeThumbnailMap public ThumbnailVodInfo(long imageUrlIndex, ImageTrack imageTrack, long mediaDurationMS, long startNumber, boolean isCatchup) { long imageMultiplier = imageUrlIndex <= 0 ? 0 : imageUrlIndex - 1; - long imageRealUrlTime = (imageMultiplier * imageTrack.getSegmentDuration()); + long imageRealUrlTime = (imageMultiplier * imageTrack.getDuration()); if (isCatchup) { - imageMultiplier = imageTrack.getStartNumber() - imageUrlIndex <= 0 ? 0 : startNumber - imageUrlIndex - 1; - imageRealUrlTime = startNumber + (imageMultiplier * imageTrack.getSegmentDuration()); + imageMultiplier = ((DashImageTrack)imageTrack).getStartNumber() - imageUrlIndex <= 0 ? 0 : startNumber - imageUrlIndex - 1; + imageRealUrlTime = startNumber + (imageMultiplier * imageTrack.getDuration()); } - String realImageUrl = imageTrack.getImageTemplateUrl().replace("$Number$", String.valueOf(imageUrlIndex)).replace("$Time$", String.valueOf(imageRealUrlTime)); + String realImageUrl = imageTrack.getUrl().replace("$Number$", String.valueOf(imageUrlIndex)).replace("$Time$", String.valueOf(imageRealUrlTime)); - long singleImageDuration = (long) Math.ceil(imageTrack.getSegmentDuration() / (imageTrack.getTilesHorizontal() * imageTrack.getTilesVertical())); + long singleImageDuration = (long) Math.ceil(imageTrack.getDuration() / (imageTrack.getCols() * imageTrack.getRows())); imageRangeThumbnailtMap = new LinkedHashMap<>(); long rangeStart = startNumber == 1 ? 0 : startNumber; - long rangeEnd = (((imageMultiplier * imageTrack.getSegmentDuration()) + singleImageDuration) - 1); + long rangeEnd = (((imageMultiplier * imageTrack.getDuration()) + singleImageDuration) - 1); long diff; if (rangeStart > rangeEnd) { rangeEnd = rangeStart + rangeEnd; } diff = rangeEnd - rangeStart; - float widthPerTile = imageTrack.getWidth() / imageTrack.getTilesHorizontal(); - float heightPerTile = imageTrack.getHeight() / imageTrack.getTilesVertical(); - for (int rowIndex = 0; rowIndex < imageTrack.getTilesVertical(); rowIndex++) { - for (int colIndex = 0; colIndex < imageTrack.getTilesHorizontal(); colIndex++) { + float widthPerTile = imageTrack.getWidth() / imageTrack.getCols(); + float heightPerTile = imageTrack.getHeight() / imageTrack.getRows(); + for (int rowIndex = 0; rowIndex < imageTrack.getRows(); rowIndex++) { + for (int colIndex = 0; colIndex < imageTrack.getCols(); colIndex++) { ImageRangeInfo imageRangeInfo = new ImageRangeInfo(rangeStart, rangeEnd); ThumbnailInfo thumbnailInfo = new ThumbnailInfo(realImageUrl, colIndex * widthPerTile, rowIndex * heightPerTile, widthPerTile, heightPerTile); - if (rangeEnd - diff > mediaDurationMS + imageTrack.getStartNumber()) { + if (rangeEnd - diff > mediaDurationMS + ((DashImageTrack)imageTrack).getStartNumber()) { continue; } imageRangeThumbnailtMap.put(imageRangeInfo, thumbnailInfo);