diff --git a/.travis.yml b/.travis.yml index a91488863..fe6cb40c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ android: before_script: - curl https://kaltura.github.io/fe-tools/android/license.sh | sh script: + - ./gradlew playkit:test - ./gradlew playkit:build after_failure: - cat playkit/build/reports/lint-results.xml diff --git a/playkit/build.gradle b/playkit/build.gradle index 3645bdf64..d51a9d5e7 100644 --- a/playkit/build.gradle +++ b/playkit/build.gradle @@ -19,10 +19,9 @@ android { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } - } - - testOptions { - unitTests.returnDefaultValues = true + debug { + testCoverageEnabled = true + } } lintOptions { @@ -32,6 +31,12 @@ android { kotlinOptions { jvmTarget = '1.8' } + + testOptions { + unitTests { + includeAndroidResources = true + } + } } tasks.withType(Javadoc) { @@ -68,7 +73,9 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'org.hamcrest:hamcrest-library:1.3' testImplementation "org.mockito:mockito-core:2.28.2" - androidTestImplementation 'androidx.test:runner:1.3.0' + testImplementation 'org.robolectric:robolectric:4.5' + testImplementation "androidx.test.ext:junit:1.1.2" + testImplementation 'androidx.test:runner:1.3.0' } if (!ext.playkitVersion.contains('dev')) { diff --git a/playkit/src/androidTest/java/com/kaltura/playkit/player/SourceSelectorAndroidTest.java b/playkit/src/androidTest/java/com/kaltura/playkit/player/SourceSelectorAndroidTest.java deleted file mode 100644 index 48837b3e6..000000000 --- a/playkit/src/androidTest/java/com/kaltura/playkit/player/SourceSelectorAndroidTest.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.kaltura.playkit.player; - -import androidx.test.runner.AndroidJUnit4; - -import com.kaltura.playkit.PKDrmParams; -import com.kaltura.playkit.PKMediaEntry; -import com.kaltura.playkit.PKMediaFormat; -import com.kaltura.playkit.PKMediaSource; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.Arrays; -import java.util.Collections; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - - -@RunWith(AndroidJUnit4.class) -public class SourceSelectorAndroidTest { - - public static final String MPD_EXAMPLE = "http://example.com/a.mpd"; - public static final String MP4_EXAMPLE = "http://example.com/a.mp4"; - public static final String M3U8_EXAMPLE = "http://example.com/a.m3u8"; - public static final String WVM_EXAMPLE = "http://example.com/a.wvm"; - public static final String MP3_EXAMPLE = "http://example.com/a.mp3"; - - - PKMediaSource dashWidevine = new PKMediaSource() - .setMediaFormat(PKMediaFormat.dash) - .setUrl("http://example.com/a.mpd") - .setDrmData(Collections.singletonList(new PKDrmParams("http://whatever", PKDrmParams.Scheme.WidevineCENC))); - PKMediaSource dashClear = new PKMediaSource().setMediaFormat(PKMediaFormat.dash).setUrl(MPD_EXAMPLE); - PKMediaSource mp4 = new PKMediaSource().setMediaFormat(PKMediaFormat.mp4).setUrl(MP4_EXAMPLE); - PKMediaSource hls = new PKMediaSource().setMediaFormat(PKMediaFormat.hls).setUrl(M3U8_EXAMPLE); - PKMediaSource wvm = new PKMediaSource().setMediaFormat(PKMediaFormat.wvm).setUrl(WVM_EXAMPLE); - PKMediaSource mp3 = new PKMediaSource().setMediaFormat(PKMediaFormat.mp3).setUrl(MP3_EXAMPLE); - - private PKMediaEntry entry(PKMediaSource... sources) { - return new PKMediaEntry().setSources(Arrays.asList(sources)); - } - - @Test - public void mediaFormatByExtension() throws Exception { - assertEquals(PKMediaFormat.dash, PKMediaFormat.valueOfExt("mpd")); - assertEquals(PKMediaFormat.mp4, PKMediaFormat.valueOfExt("mp4")); - assertEquals(PKMediaFormat.hls, PKMediaFormat.valueOfExt("m3u8")); - assertEquals(PKMediaFormat.wvm, PKMediaFormat.valueOfExt("wvm")); - assertEquals(null, PKMediaFormat.valueOfExt("foo")); - assertEquals(null, PKMediaFormat.valueOfExt(null)); - - - assertEquals(PKMediaFormat.dash, PKMediaFormat.valueOfUrl(dashClear.getUrl())); - assertEquals(PKMediaFormat.mp4, PKMediaFormat.valueOfUrl(mp4.getUrl())); - assertEquals(PKMediaFormat.hls, PKMediaFormat.valueOfUrl(hls.getUrl())); - assertEquals(PKMediaFormat.wvm, PKMediaFormat.valueOfUrl(wvm.getUrl())); - - } - - @Test - public void sourceSelector() { - - assertTrue(SourceSelector.selectSource(entry(mp4, dashClear), PKMediaFormat.dash) == dashClear); - assertTrue(SourceSelector.selectSource(entry(mp4, hls), PKMediaFormat.hls) == hls); - assertTrue(SourceSelector.selectSource(entry(hls, mp4, dashClear), PKMediaFormat.dash) == dashClear); - assertTrue(SourceSelector.selectSource(entry(hls, wvm), PKMediaFormat.dash) == hls); - assertTrue(SourceSelector.selectSource(entry(dashWidevine, wvm, hls, mp4), PKMediaFormat.wvm) == dashWidevine); - assertTrue(SourceSelector.selectSource(entry(mp3), PKMediaFormat.mp3) == mp3); -// assertTrue(SourceSelector.selectSource(entry(mp4, wvm)) == wvm); - - } -} 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 1a125b8cf..3b306669a 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/ExoPlayerWrapper.java @@ -1006,7 +1006,7 @@ public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray tra shouldGetTracksInfo = !trackSelectionHelper.prepareTracks(trackSelections, customDashManifest); } } - + trackSelectionHelper.notifyAboutTrackChange(trackSelections); } 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 4cdd8fe7b..2c0166cb7 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/TrackSelectionHelper.java @@ -96,28 +96,27 @@ class TrackSelectionHelper { private final Context context; private final DefaultTrackSelector selector; private TrackSelectionArray trackSelectionArray; - private MappingTrackSelector.MappedTrackInfo mappedTrackInfo; + MappingTrackSelector.MappedTrackInfo mappedTrackInfo; - private List videoTracks = new ArrayList<>(); + List videoTracks = new ArrayList<>(); private List originalVideoTracks; - private List audioTracks = new ArrayList<>(); - private List textTracks = new ArrayList<>(); - private List imageTracks = new ArrayList<>(); + List audioTracks = new ArrayList<>(); + List textTracks = new ArrayList<>(); + List imageTracks = new ArrayList<>(); - - private Map>> subtitleListMap = new HashMap<>(); - private Map> videoTracksCodecsMap = new HashMap<>(); - private Map> audioTracksCodecsMap = new HashMap<>(); + Map>> subtitleListMap = new HashMap<>(); + Map> videoTracksCodecsMap = new HashMap<>(); + Map> audioTracksCodecsMap = new HashMap<>(); private String[] lastSelectedTrackIds; private String[] requestedChangeTrackIds; // To know if application passed the external subtitles - private boolean hasExternalSubtitles = false; + boolean hasExternalSubtitles = false; // To know if tracks has external subtitles or not. // Helpful in case if subtitles are removed by ExoPlayer in case of any discrepancy - private boolean hasExternalSubtitlesInTracks = false; + boolean hasExternalSubtitlesInTracks = false; private TracksInfoListener tracksInfoListener; private TracksErrorListener tracksErrorListener; @@ -224,7 +223,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(List rawImageTracks) { + PKTracks buildTracks(List rawImageTracks) { clearTracksLists(); @@ -392,18 +391,18 @@ private TrackType getTrackType(int rendererIndex) { return trackType; } - private String getLanguageFromFormat(Format format) { + String getLanguageFromFormat(Format format) { if (format.language == null) { return LANGUAGE_UNKNOWN; } return format.language; } - private boolean isExternalSubtitle(String language, String sampleMimeType) { + boolean isExternalSubtitle(String language, String sampleMimeType) { return language != null && (language.contains("-" + sampleMimeType) || language.contains("-" + "Unknown")); } - private String getExternalSubtitleLanguage(Format format) { + String getExternalSubtitleLanguage(Format format) { if (format.language != null) { return format.language.substring(0, format.language.indexOf("-")); } else { @@ -411,7 +410,7 @@ private String getExternalSubtitleLanguage(Format format) { } } - private boolean discardTextTrackOnPreference(Format format) { + boolean discardTextTrackOnPreference(Format format) { PKSubtitlePreference subtitlePreference = playerSettings.getSubtitlePreference(); if (subtitlePreference == PKSubtitlePreference.OFF) { return false; @@ -442,7 +441,7 @@ private boolean discardTextTrackOnPreference(Format format) { return false; } - private List filterVideoTracks() { + List filterVideoTracks() { if (videoTracksCodecsMap == null || videoTracksCodecsMap.isEmpty()) { return videoTracks; @@ -485,7 +484,7 @@ private List filterVideoTracks() { return videoTracks; } - private boolean videoCodecsSupportedInHardware() { + boolean videoCodecsSupportedInHardware() { for (PKVideoCodec videoCodecForPlayback : playerSettings.getPreferredVideoCodecSettings().getCodecPriorityList()) { if (!videoTracksCodecsMap.containsKey(videoCodecForPlayback)) { continue; @@ -504,7 +503,7 @@ private boolean videoCodecsSupportedInHardware() { return false; } - private void populateAllCodecTracks(boolean atleastOneCodecSupportedInHardware) { + void populateAllCodecTracks(boolean atleastOneCodecSupportedInHardware) { for (Map.Entry> multipleCodecEntry : videoTracksCodecsMap.entrySet()) { for (VideoTrack codecVideoTrack : multipleCodecEntry.getValue()) { if (codecVideoTrack.getCodecName() != null && @@ -524,7 +523,7 @@ private void populateAllCodecTracks(boolean atleastOneCodecSupportedInHardware) * and hide all the bitrate variants. * if app prefers to allow audio track selection by codec type it will return all available tracks by codec by audio groups */ - private ArrayList filterAdaptiveAudioTracks() { + ArrayList filterAdaptiveAudioTracks() { ArrayList filteredAudioTracks = new ArrayList<>(); AudioTrack audioTrack; @@ -563,8 +562,7 @@ private ArrayList filterAdaptiveAudioTracks() { * Will add this track only if there is at least one text track available. * Selecting this track, will disable the text renderer. */ - private void maybeAddDisabledTextTrack() { - + void maybeAddDisabledTextTrack() { if (textTracks.isEmpty()) { return; } @@ -580,7 +578,7 @@ private void maybeAddDisabledTextTrack() { * @return - the index of the track that is selected by default or lastSelected track(Depending on the use-case), * or 0 if no default selection is available and no track was previously selected. */ - private int getDefaultTrackIndex(List trackList, String lastSelectedTrackId) { + int getDefaultTrackIndex(List trackList, String lastSelectedTrackId) { int defaultTrackIndex = 0; @@ -636,7 +634,7 @@ private int getDefaultTrackIndex(List trackList, String las * * @param trackGroupArray TrackGroupArray this includes Internal and External Text Formats */ - private void extractTextTracksToMap(TrackGroupArray trackGroupArray) { + void extractTextTracksToMap(TrackGroupArray trackGroupArray) { for (int groupIndex = 0; groupIndex < trackGroupArray.length; groupIndex++) { TrackGroup trackGroup = trackGroupArray.get(groupIndex); @@ -670,7 +668,7 @@ private void extractTextTracksToMap(TrackGroupArray trackGroupArray) { } } - private int getUpdatedDefaultTrackIndex(List trackList, int defaultTrackIndex) { + int getUpdatedDefaultTrackIndex(List trackList, int defaultTrackIndex) { if (!trackList.isEmpty()) { int trackType = TRACK_TYPE_UNKNOWN; @@ -691,7 +689,7 @@ private int getUpdatedDefaultTrackIndex(List trackList, int return defaultTrackIndex; } - private int findDefaultTrackIndex(String selectedFormatLanguage, List trackList, int defaultTrackIndex) { + int findDefaultTrackIndex(String selectedFormatLanguage, List trackList, int defaultTrackIndex) { if (trackList != null && selectedFormatLanguage != null) { for (int i = 0; i < trackList.size(); i++) { @@ -714,7 +712,7 @@ private int findDefaultTrackIndex(String selectedFormatLanguage, List trackList, String lastSelectedTrackId, int defaultTrackIndex) { + int restoreLastSelectedTrack(List trackList, String lastSelectedTrackId, int defaultTrackIndex) { //If track was previously selected and selection is differed from the default selection apply it. String defaultUniqueId = trackList.get(defaultTrackIndex).getUniqueId(); if (!NONE.equals(lastSelectedTrackId) && !lastSelectedTrackId.equals(defaultUniqueId)) { @@ -736,7 +734,7 @@ private int restoreLastSelectedTrack(List trackList, String * @param groupIndex - the index of the group this adaptive object refer. * @param format - the actual format of the adaptive object. */ - private void maybeAddAdaptiveTrack(int rendererIndex, int groupIndex, Format format) { + void maybeAddAdaptiveTrack(int rendererIndex, int groupIndex, Format format) { String uniqueId = getUniqueId(rendererIndex, groupIndex, TRACK_ADAPTIVE); if (isAdaptive(rendererIndex, groupIndex) && !adaptiveTrackAlreadyExist(uniqueId, rendererIndex)) { switch (rendererIndex) { @@ -765,7 +763,7 @@ private void maybeAddAdaptiveTrack(int rendererIndex, int groupIndex, Format for * @param trackIndex - actual track index. * @return - uniqueId that represent current track. */ - private String getUniqueId(int rendererIndex, int groupIndex, int trackIndex) { + String getUniqueId(int rendererIndex, int groupIndex, int trackIndex) { return getUniqueIdPrefix(rendererIndex) + rendererIndex + "," @@ -774,7 +772,7 @@ private String getUniqueId(int rendererIndex, int groupIndex, int trackIndex) { + getUniqueIdPostfix(rendererIndex, trackIndex); } - private String getUniqueIdPrefix(int rendererIndex) { + String getUniqueIdPrefix(int rendererIndex) { switch (rendererIndex) { case TRACK_TYPE_VIDEO: return VIDEO_PREFIX; @@ -789,7 +787,7 @@ private String getUniqueIdPrefix(int rendererIndex) { } } - private String getUniqueIdPostfix(int rendererIndex, int trackIndex) { + String getUniqueIdPostfix(int rendererIndex, int trackIndex) { switch (rendererIndex) { case TRACK_ADAPTIVE: return ADAPTIVE; @@ -833,12 +831,11 @@ protected void changeTrack(String uniqueId) { parametersBuilder.setRendererDisabled(TRACK_TYPE_TEXT, uniqueTrackId[TRACK_INDEX] == TRACK_DISABLED); } - SelectionOverride override = retrieveOverrideSelection(uniqueTrackId); overrideTrack(rendererIndex, override, parametersBuilder); } - protected void overrideMediaVideoCodec() { + void overrideMediaVideoCodec() { List uniqueIds = getVideoTracksUniqueIds(); @@ -878,7 +875,7 @@ protected void overrideMediaDefaultABR(long minAbr, long maxAbr, PKAbrFilter pkA overrideTrack(rendererIndex, override, parametersBuilder); } - private List getVideoTracksUniqueIds() { + List getVideoTracksUniqueIds() { List uniqueIds = new ArrayList<>(); if (videoTracks != null) { Collections.sort(videoTracks); @@ -889,7 +886,7 @@ private List getVideoTracksUniqueIds() { return uniqueIds; } - private List getCodecUniqueIdsWithABR(long minAbr, long maxAbr, PKAbrFilter pkAbrFilter) { + List getCodecUniqueIdsWithABR(long minAbr, long maxAbr, PKAbrFilter pkAbrFilter) { List uniqueIds = new ArrayList<>(); boolean isValidABRRange = true; @@ -993,7 +990,7 @@ private List getCodecUniqueIdsWithABR(long minAbr, long maxAbr, PKAbrFil return uniqueIds; } - private SelectionOverride retrieveOverrideSelectionList(int[][] uniqueIds) { + SelectionOverride retrieveOverrideSelectionList(int[][] uniqueIds) { if (uniqueIds == null || uniqueIds[0] == null) { throw new IllegalArgumentException("Track selection with uniqueId = null"); } @@ -1017,7 +1014,7 @@ private SelectionOverride retrieveOverrideSelectionList(int[][] uniqueIds) { } @NonNull - private SelectionOverride overrideMediaDefaultABR(int[][] uniqueIds, int rendererIndex, int groupIndex) { + SelectionOverride overrideMediaDefaultABR(int[][] uniqueIds, int rendererIndex, int groupIndex) { SelectionOverride override; int[] adaptiveTrackIndexes; List adaptiveTrackIndexesList = new ArrayList<>(); @@ -1033,7 +1030,7 @@ private SelectionOverride overrideMediaDefaultABR(int[][] uniqueIds, int rendere return override; } - private void createAdaptiveTrackIndexList(int[][] uniqueIds, int groupIndex, List adaptiveTrackIndexesList) { + void createAdaptiveTrackIndexList(int[][] uniqueIds, int groupIndex, List adaptiveTrackIndexesList) { int trackIndex; int trackGroupIndex; @@ -1050,7 +1047,7 @@ private void createAdaptiveTrackIndexList(int[][] uniqueIds, int groupIndex, Lis } - private int[][] validateAndBuildUniqueIds(List uniqueIds) { + int[][] validateAndBuildUniqueIds(List uniqueIds) { int [][] idsList = new int [uniqueIds.size()][TRACK_RENDERERS_AMOUNT]; for (int index = 0 ; index < idsList.length ; index++) { int[] uniqueTrackId = validateUniqueId(uniqueIds.get(index)); @@ -1063,7 +1060,7 @@ private int[][] validateAndBuildUniqueIds(List uniqueIds) { * @param uniqueId - the uniqueId to convert. * @return - int[] that consist from indexes that are readable to Exoplayer. */ - private int[] parseUniqueId(String uniqueId) { + int[] parseUniqueId(String uniqueId) { int[] parsedUniqueId = new int[3]; String splitUniqueId = removePrefix(uniqueId); String[] strArray = splitUniqueId.split(","); @@ -1092,7 +1089,7 @@ private int[] parseUniqueId(String uniqueId) { * @param uniqueId - the unique id of the track that will override the existing one. * @return - the {@link SelectionOverride} which will override the existing selection. */ - private SelectionOverride retrieveOverrideSelection(int[] uniqueId) { + SelectionOverride retrieveOverrideSelection(int[] uniqueId) { SelectionOverride override; @@ -1162,8 +1159,9 @@ private SelectionOverride retrieveOverrideSelection(int[] uniqueId) { } @NonNull - private SelectionOverride overrideAutoABRTracks(int rendererIndex, int groupIndex) { - SelectionOverride override;List adaptiveTrackIndexesList = new ArrayList<>(); + SelectionOverride overrideAutoABRTracks(int rendererIndex, int groupIndex) { + SelectionOverride override; + List adaptiveTrackIndexesList = new ArrayList<>(); int[] adaptiveTrackIndexes; switch (rendererIndex) { @@ -1214,7 +1212,7 @@ private SelectionOverride overrideAutoABRTracks(int rendererIndex, int groupInde * @param rendererIndex - renderer index on which we want to apply the change. * @param override - the new selection with which we want to override the currently active track. */ - private void overrideTrack(int rendererIndex, SelectionOverride override, DefaultTrackSelector.ParametersBuilder parametersBuilder) { + void overrideTrack(int rendererIndex, SelectionOverride override, DefaultTrackSelector.ParametersBuilder parametersBuilder) { if (override != null) { //actually change track. TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); @@ -1226,7 +1224,7 @@ private void overrideTrack(int rendererIndex, SelectionOverride override, Defaul selector.setParameters(parametersBuilder); } - public void updateTrackSelectorParameter(PlayerSettings playerSettings, DefaultTrackSelector.ParametersBuilder parametersBuilder) { + void updateTrackSelectorParameter(PlayerSettings playerSettings, DefaultTrackSelector.ParametersBuilder parametersBuilder) { if (playerSettings == null) { return; } @@ -1289,7 +1287,7 @@ public ThumbnailInfo getThumbnailInfo(long positionMS) { * @param rendererIndex - renderer index. * @return - true, if adaptive {@link BaseTrack} object already exist for this group. */ - private boolean adaptiveTrackAlreadyExist(String uniqueId, int rendererIndex) { + boolean adaptiveTrackAlreadyExist(String uniqueId, int rendererIndex) { List trackList = new ArrayList<>(); switch (rendererIndex) { @@ -1316,7 +1314,7 @@ private boolean adaptiveTrackAlreadyExist(String uniqueId, int rendererIndex) { return false; } - private int getIndexFromUniqueId(String uniqueId, int groupIndex) { + int getIndexFromUniqueId(String uniqueId, int groupIndex) { String uniqueIdWithoutPrefix = removePrefix(uniqueId); String[] strArray = uniqueIdWithoutPrefix.split(","); if (strArray[groupIndex].equals(ADAPTIVE)) { @@ -1326,13 +1324,13 @@ private int getIndexFromUniqueId(String uniqueId, int groupIndex) { return Integer.valueOf(strArray[groupIndex]); } - private String removePrefix(String uniqueId) { + String removePrefix(String uniqueId) { String[] strArray = uniqueId.split(":"); //always return the second element of the splitString. return strArray[1]; } - private int[] convertAdaptiveListToArray(List adaptiveTrackIndexesList) { + int[] convertAdaptiveListToArray(List adaptiveTrackIndexesList) { int[] adaptiveTrackIndexes = new int[adaptiveTrackIndexesList.size()]; for (int i = 0; i < adaptiveTrackIndexes.length; i++) { adaptiveTrackIndexes[i] = adaptiveTrackIndexesList.get(i); @@ -1341,12 +1339,12 @@ private int[] convertAdaptiveListToArray(List adaptiveTrackIndexesList) return adaptiveTrackIndexes; } - private boolean isFormatSupported(int rendererCount, int groupIndex, int trackIndex) { + boolean isFormatSupported(int rendererCount, int groupIndex, int trackIndex) { return mappedTrackInfo.getTrackSupport(rendererCount, groupIndex, trackIndex) == Consts.FORMAT_HANDLED; } - private boolean isAdaptive(int rendererIndex, int groupIndex) { + boolean isAdaptive(int rendererIndex, int groupIndex) { TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); return mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false) != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED @@ -1360,9 +1358,8 @@ private boolean isAdaptive(int rendererIndex, int groupIndex) { * @return - parsed uniqueId in case of success. * @throws IllegalArgumentException when uniqueId is illegal. */ - private int[] validateUniqueId(String uniqueId) throws IllegalArgumentException { - - if (uniqueId == null) { + int[] validateUniqueId(String uniqueId) throws IllegalArgumentException { + if (TextUtils.isEmpty(uniqueId)) { throw new IllegalArgumentException("uniqueId is null"); } @@ -1389,7 +1386,7 @@ private int[] validateUniqueId(String uniqueId) throws IllegalArgumentException throw new IllegalArgumentException("Invalid structure of uniqueId " + uniqueId); } - private boolean isTrackIndexValid(int[] parsedUniqueId) { + boolean isTrackIndexValid(int[] parsedUniqueId) { int rendererIndex = parsedUniqueId[RENDERER_INDEX]; int groupIndex = parsedUniqueId[GROUP_INDEX]; int trackIndex = parsedUniqueId[TRACK_INDEX]; @@ -1408,7 +1405,7 @@ private boolean isTrackIndexValid(int[] parsedUniqueId) { && trackIndex < mappedTrackInfo.getTrackGroups(rendererIndex).get(groupIndex).length; } - private boolean isGroupIndexValid(int[] parsedUniqueId) { + boolean isGroupIndexValid(int[] parsedUniqueId) { if (parsedUniqueId[GROUP_INDEX] == TRACK_TYPE_IMAGE) { return true; } @@ -1532,7 +1529,7 @@ protected long getCurrentVideoHeight() { return -1; } - private ExoTrackSelection getTrackSelection(TrackSelection trackSelection) { + ExoTrackSelection getTrackSelection(TrackSelection trackSelection) { if (trackSelection instanceof ExoTrackSelection) { return (ExoTrackSelection) trackSelection; } @@ -1649,7 +1646,7 @@ protected String getPreferredTrackId(int trackType) { } @Nullable - private String getPreferredTextTrackUniqueId(int trackType) { + String getPreferredTextTrackUniqueId(int trackType) { if (!isValidPreferredTextConfig()) { return null; } @@ -1695,7 +1692,7 @@ private String getPreferredTextTrackUniqueId(int trackType) { } @Nullable - private String maybeSetFirstTextTrackAsAutoSelection() { + String maybeSetFirstTextTrackAsAutoSelection() { String preferredTrackUniqueId = null; //if user set mode to AUTO and the locale lang is not in the stream and no default text track in the stream so we will not select None but the first text track in the stream @@ -1715,7 +1712,7 @@ private String maybeSetFirstTextTrackAsAutoSelection() { return preferredTrackUniqueId; } - private String getPreferredAudioTrackUniqueId(int trackType) { + String getPreferredAudioTrackUniqueId(int trackType) { if (!isValidPreferredAudioConfig()) { return null; } @@ -1746,7 +1743,7 @@ private String getPreferredAudioTrackUniqueId(int trackType) { return preferredTrackUniqueId; } - private PKVideoCodec getVideoCodec(Format format) { + PKVideoCodec getVideoCodec(Format format) { String codec = format.codecs; if (codec != null) { if (codec.startsWith("hev1") || codec.startsWith("hvc1")) { @@ -1773,7 +1770,7 @@ private PKVideoCodec getVideoCodec(Format format) { return PKVideoCodec.AVC; //VIDEO_H264 } - private PKAudioCodec getAudioCodec(Format format) { + PKAudioCodec getAudioCodec(Format format) { String codec = format.codecs; if (codec != null) { if (codec.startsWith("mp4a")) { @@ -1789,7 +1786,7 @@ private PKAudioCodec getAudioCodec(Format format) { return PKAudioCodec.AAC; } - private boolean isValidPreferredAudioConfig() { + boolean isValidPreferredAudioConfig() { if (playerSettings == null) { return false; } @@ -1801,7 +1798,7 @@ private boolean isValidPreferredAudioConfig() { (preferredAudioTrackConfig.getPreferredMode() == PKTrackConfig.Mode.SELECTION && preferredAudioTrackConfig.getTrackLanguage() == null)); } - private boolean isValidPreferredTextConfig() { + boolean isValidPreferredTextConfig() { if (playerSettings == null) { return false; } @@ -1820,7 +1817,7 @@ protected void hasExternalSubtitles(boolean hasExternalSubtitles) { this.hasExternalSubtitles = hasExternalSubtitles; } - public static boolean isCodecSupported(@NonNull String codecs, @Nullable TrackType type, boolean allowSoftware) { + boolean isCodecSupported(@NonNull String codecs, @Nullable TrackType type, boolean allowSoftware) { if (type == TrackType.TEXT) { return true; // always supported @@ -1847,9 +1844,4 @@ public static boolean isCodecSupported(@NonNull String codecs, @Nullable TrackTy return PKCodecSupport.hasDecoder(codecs, false, allowSoftware); } } - - - - - } diff --git a/playkit/src/main/java/com/kaltura/testhelper/FakeRendererCapabilities.kt b/playkit/src/main/java/com/kaltura/testhelper/FakeRendererCapabilities.kt new file mode 100644 index 000000000..bf14a4f44 --- /dev/null +++ b/playkit/src/main/java/com/kaltura/testhelper/FakeRendererCapabilities.kt @@ -0,0 +1,47 @@ +package com.kaltura.testhelper + +import com.kaltura.android.exoplayer2.C.FORMAT_HANDLED +import com.kaltura.android.exoplayer2.C.FORMAT_UNSUPPORTED_TYPE +import com.kaltura.android.exoplayer2.Format +import com.kaltura.android.exoplayer2.RendererCapabilities +import com.kaltura.android.exoplayer2.RendererCapabilities.ADAPTIVE_SEAMLESS +import com.kaltura.android.exoplayer2.RendererCapabilities.TUNNELING_NOT_SUPPORTED +import com.kaltura.android.exoplayer2.util.MimeTypes +import com.kaltura.android.exoplayer2.util.Util + +/** + * Returns [FakeRendererCapabilities] that advertises support level using given value for + * all tracks of the given type. + * + * @param trackType the track type of all formats that this renderer capabilities advertises + * support for. + * @param supportValue the [Capabilities] that will be returned for formats with the given + * type. + */ +class FakeRendererCapabilities(private var trackType: Int) : RendererCapabilities { + + @RendererCapabilities.Capabilities + private var supportValue = 0 + + init { + this.supportValue = RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED) + } + + override fun getName(): String { + return "FakeRenderer(" + Util.getTrackTypeString(trackType).toString() + ")" + } + + override fun getTrackType(): Int { + return trackType + } + + @RendererCapabilities.Capabilities + override fun supportsFormat(format: Format): Int { + return if (MimeTypes.getTrackType(format.sampleMimeType) === trackType) supportValue else RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE) + } + + @RendererCapabilities.AdaptiveSupport + override fun supportsMixedMimeTypeAdaptation(): Int { + return ADAPTIVE_SEAMLESS + } +} diff --git a/playkit/src/main/java/com/kaltura/testhelper/FakeShuffleOrder.java b/playkit/src/main/java/com/kaltura/testhelper/FakeShuffleOrder.java new file mode 100644 index 000000000..a040150cb --- /dev/null +++ b/playkit/src/main/java/com/kaltura/testhelper/FakeShuffleOrder.java @@ -0,0 +1,57 @@ +package com.kaltura.testhelper; + +import com.kaltura.android.exoplayer2.C; +import com.kaltura.android.exoplayer2.source.ShuffleOrder; + +/** + * Fake {@link ShuffleOrder} which returns a reverse order. This order is thus deterministic but + * different from the original order. + */ +public final class FakeShuffleOrder implements ShuffleOrder { + + private final int length; + + public FakeShuffleOrder(int length) { + this.length = length; + } + + @Override + public int getLength() { + return length; + } + + @Override + public int getNextIndex(int index) { + return index > 0 ? index - 1 : C.INDEX_UNSET; + } + + @Override + public int getPreviousIndex(int index) { + return index < length - 1 ? index + 1 : C.INDEX_UNSET; + } + + @Override + public int getLastIndex() { + return length > 0 ? 0 : C.INDEX_UNSET; + } + + @Override + public int getFirstIndex() { + return length > 0 ? length - 1 : C.INDEX_UNSET; + } + + @Override + public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) { + return new FakeShuffleOrder(length + insertionCount); + } + + @Override + public ShuffleOrder cloneAndRemove(int indexFrom, int indexToExclusive) { + return new FakeShuffleOrder(length - indexToExclusive + indexFrom); + } + + @Override + public ShuffleOrder cloneAndClear() { + return new FakeShuffleOrder(/* length= */ 0); + } +} diff --git a/playkit/src/main/java/com/kaltura/testhelper/FakeTimeline.java b/playkit/src/main/java/com/kaltura/testhelper/FakeTimeline.java new file mode 100644 index 000000000..a7085f50c --- /dev/null +++ b/playkit/src/main/java/com/kaltura/testhelper/FakeTimeline.java @@ -0,0 +1,440 @@ +package com.kaltura.testhelper; + +import android.net.Uri; +import android.util.Pair; + +import com.kaltura.android.exoplayer2.C; +import com.kaltura.android.exoplayer2.MediaItem; +import com.kaltura.android.exoplayer2.Player; +import com.kaltura.android.exoplayer2.Timeline; +import com.kaltura.android.exoplayer2.source.ads.AdPlaybackState; +import com.kaltura.android.exoplayer2.util.Assertions; +import com.kaltura.android.exoplayer2.util.Util; + +import java.util.Arrays; + +import static java.lang.Math.min; + +/** Fake {@link Timeline} which can be setup to return custom {@link TimelineWindowDefinition}s. */ +public final class FakeTimeline extends Timeline { + + /** + * Definition used to define a {@link FakeTimeline}. + */ + public static final class TimelineWindowDefinition { + + /** Default window duration in microseconds. */ + public static final long DEFAULT_WINDOW_DURATION_US = 10 * C.MICROS_PER_SECOND; + + /** Default offset of a window in its first period in microseconds. */ + public static final long DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US = 123 * C.MICROS_PER_SECOND; + + public final int periodCount; + public final Object id; + public final MediaItem mediaItem; + public final boolean isSeekable; + public final boolean isDynamic; + public final boolean isLive; + public final boolean isPlaceholder; + public final long durationUs; + public final long defaultPositionUs; + public final long windowOffsetInFirstPeriodUs; + public final AdPlaybackState adPlaybackState; + + /** + * Creates a window definition that corresponds to a placeholder timeline using the given tag. + * + * @param tag The tag to use in the timeline. + */ + public static TimelineWindowDefinition createPlaceholder(Object tag) { + return new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ tag, + /* isSeekable= */ false, + /* isDynamic= */ true, + /* isLive= */ false, + /* isPlaceholder= */ true, + /* durationUs= */ C.TIME_UNSET, + /* defaultPositionUs= */ 0, + /* windowOffsetInFirstPeriodUs= */ 0, + AdPlaybackState.NONE); + } + + /** + * Creates a seekable, non-dynamic window definition with a duration of {@link + * #DEFAULT_WINDOW_DURATION_US}. + * + * @param periodCount The number of periods in the window. Each period get an equal slice of the + * total window duration. + * @param id The UID of the window. + */ + public TimelineWindowDefinition(int periodCount, Object id) { + this(periodCount, id, true, false, DEFAULT_WINDOW_DURATION_US); + } + + /** + * Creates a window definition with one period. + * + * @param isSeekable Whether the window is seekable. + * @param isDynamic Whether the window is dynamic. + * @param durationUs The duration of the window in microseconds. + */ + public TimelineWindowDefinition(boolean isSeekable, boolean isDynamic, long durationUs) { + this(1, 0, isSeekable, isDynamic, durationUs); + } + + /** + * Creates a window definition. + * + * @param periodCount The number of periods in the window. Each period get an equal slice of the + * total window duration. + * @param id The UID of the window. + * @param isSeekable Whether the window is seekable. + * @param isDynamic Whether the window is dynamic. + * @param durationUs The duration of the window in microseconds. + */ + public TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable, + boolean isDynamic, long durationUs) { + this(periodCount, id, isSeekable, isDynamic, durationUs, AdPlaybackState.NONE); + } + + /** + * Creates a window definition with ad groups. + * + * @param periodCount The number of periods in the window. Each period get an equal slice of the + * total window duration. + * @param id The UID of the window. + * @param isSeekable Whether the window is seekable. + * @param isDynamic Whether the window is dynamic. + * @param durationUs The duration of the window in microseconds. + * @param adPlaybackState The ad playback state. + */ + public TimelineWindowDefinition( + int periodCount, + Object id, + boolean isSeekable, + boolean isDynamic, + long durationUs, + AdPlaybackState adPlaybackState) { + this( + periodCount, + id, + isSeekable, + isDynamic, + /* isLive= */ isDynamic, + /* isPlaceholder= */ false, + durationUs, + /* defaultPositionUs= */ 0, + DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, + adPlaybackState); + } + + /** + * Creates a window definition with ad groups. + * + * @param periodCount The number of periods in the window. Each period get an equal slice of the + * total window duration. + * @param id The UID of the window. + * @param isSeekable Whether the window is seekable. + * @param isDynamic Whether the window is dynamic. + * @param isLive Whether the window is live. + * @param isPlaceholder Whether the window is a placeholder. + * @param durationUs The duration of the window in microseconds. + * @param defaultPositionUs The default position of the window in microseconds. + * @param windowOffsetInFirstPeriodUs The offset of the window in the first period, in + * microseconds. + * @param adPlaybackState The ad playback state. + */ + public TimelineWindowDefinition( + int periodCount, + Object id, + boolean isSeekable, + boolean isDynamic, + boolean isLive, + boolean isPlaceholder, + long durationUs, + long defaultPositionUs, + long windowOffsetInFirstPeriodUs, + AdPlaybackState adPlaybackState) { + this( + periodCount, + id, + isSeekable, + isDynamic, + isLive, + isPlaceholder, + durationUs, + defaultPositionUs, + windowOffsetInFirstPeriodUs, + adPlaybackState, + FAKE_MEDIA_ITEM.buildUpon().setTag(id).build()); + } + + /** + * Creates a window definition with ad groups and a custom media item. + * + * @param periodCount The number of periods in the window. Each period get an equal slice of the + * total window duration. + * @param id The UID of the window. + * @param isSeekable Whether the window is seekable. + * @param isDynamic Whether the window is dynamic. + * @param isLive Whether the window is live. + * @param isPlaceholder Whether the window is a placeholder. + * @param durationUs The duration of the window in microseconds. + * @param defaultPositionUs The default position of the window in microseconds. + * @param windowOffsetInFirstPeriodUs The offset of the window in the first period, in + * microseconds. + * @param adPlaybackState The ad playback state. + * @param mediaItem The media item to include in the timeline. + */ + public TimelineWindowDefinition( + int periodCount, + Object id, + boolean isSeekable, + boolean isDynamic, + boolean isLive, + boolean isPlaceholder, + long durationUs, + long defaultPositionUs, + long windowOffsetInFirstPeriodUs, + AdPlaybackState adPlaybackState, + MediaItem mediaItem) { + Assertions.checkArgument(durationUs != C.TIME_UNSET || periodCount == 1); + this.periodCount = periodCount; + this.id = id; + this.mediaItem = mediaItem; + this.isSeekable = isSeekable; + this.isDynamic = isDynamic; + this.isLive = isLive; + this.isPlaceholder = isPlaceholder; + this.durationUs = durationUs; + this.defaultPositionUs = defaultPositionUs; + this.windowOffsetInFirstPeriodUs = windowOffsetInFirstPeriodUs; + this.adPlaybackState = adPlaybackState; + } + } + + /** The fake media item used by the fake timeline. */ + public static final MediaItem FAKE_MEDIA_ITEM = + new MediaItem.Builder().setMediaId("FakeTimeline").setUri(Uri.EMPTY).build(); + + private static final long AD_DURATION_US = 5 * C.MICROS_PER_SECOND; + + private final TimelineWindowDefinition[] windowDefinitions; + private final Object[] manifests; + private final int[] periodOffsets; + private final FakeShuffleOrder fakeShuffleOrder; + + /** + * Returns an ad playback state with the specified number of ads in each of the specified ad + * groups, each ten seconds long. + * + * @param adsPerAdGroup The number of ads per ad group. + * @param adGroupTimesUs The times of ad groups, in microseconds. + * @return The ad playback state. + */ + public static AdPlaybackState createAdPlaybackState(int adsPerAdGroup, long... adGroupTimesUs) { + int adGroupCount = adGroupTimesUs.length; + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ new Object(), adGroupTimesUs); + long[][] adDurationsUs = new long[adGroupCount][]; + for (int i = 0; i < adGroupCount; i++) { + adPlaybackState = adPlaybackState.withAdCount(/* adGroupIndex= */ i, adsPerAdGroup); + for (int j = 0; j < adsPerAdGroup; j++) { + adPlaybackState = + adPlaybackState.withAdUri( + /* adGroupIndex= */ i, + /* adIndexInAdGroup= */ j, + Uri.parse("https://example.com/ad/" + i + "/" + j)); + } + adDurationsUs[i] = new long[adsPerAdGroup]; + Arrays.fill(adDurationsUs[i], AD_DURATION_US); + } + adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs); + + return adPlaybackState; + } + + /** + * Create a fake timeline with one seekable, non-dynamic window with one period and a duration of + * {@link TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US}. + */ + public FakeTimeline() { + this(/* windowCount= */ 1); + } + + /** + * Creates a fake timeline with the given number of seekable, non-dynamic windows with one period + * with a duration of {@link TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US} each. + * + * @param windowCount The number of windows. + * @param manifests The manifests of the windows. + */ + public FakeTimeline(int windowCount, Object... manifests) { + this(manifests, createDefaultWindowDefinitions(windowCount)); + } + + /** + * Creates a fake timeline with the given window definitions. + * + * @param windowDefinitions A list of {@link TimelineWindowDefinition}s. + */ + public FakeTimeline(TimelineWindowDefinition... windowDefinitions) { + this(new Object[0], windowDefinitions); + } + + /** + * Creates a fake timeline with the given window definitions. + * + * @param windowDefinitions A list of {@link TimelineWindowDefinition}s. + */ + public FakeTimeline(Object[] manifests, TimelineWindowDefinition... windowDefinitions) { + this.manifests = new Object[windowDefinitions.length]; + System.arraycopy(manifests, 0, this.manifests, 0, min(this.manifests.length, manifests.length)); + this.windowDefinitions = windowDefinitions; + periodOffsets = new int[windowDefinitions.length + 1]; + periodOffsets[0] = 0; + for (int i = 0; i < windowDefinitions.length; i++) { + periodOffsets[i + 1] = periodOffsets[i] + windowDefinitions[i].periodCount; + } + fakeShuffleOrder = new FakeShuffleOrder(windowDefinitions.length); + } + + @Override + public int getWindowCount() { + return windowDefinitions.length; + } + + @Override + public int getNextWindowIndex( + int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { + if (repeatMode == Player.REPEAT_MODE_ONE) { + return windowIndex; + } + if (windowIndex == getLastWindowIndex(shuffleModeEnabled)) { + return repeatMode == Player.REPEAT_MODE_ALL + ? getFirstWindowIndex(shuffleModeEnabled) + : C.INDEX_UNSET; + } + return shuffleModeEnabled ? fakeShuffleOrder.getNextIndex(windowIndex) : windowIndex + 1; + } + + @Override + public int getPreviousWindowIndex( + int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { + if (repeatMode == Player.REPEAT_MODE_ONE) { + return windowIndex; + } + if (windowIndex == getFirstWindowIndex(shuffleModeEnabled)) { + return repeatMode == Player.REPEAT_MODE_ALL + ? getLastWindowIndex(shuffleModeEnabled) + : C.INDEX_UNSET; + } + return shuffleModeEnabled ? fakeShuffleOrder.getPreviousIndex(windowIndex) : windowIndex - 1; + } + + @Override + public int getLastWindowIndex(boolean shuffleModeEnabled) { + return shuffleModeEnabled + ? fakeShuffleOrder.getLastIndex() + : super.getLastWindowIndex(/* shuffleModeEnabled= */ false); + } + + @Override + public int getFirstWindowIndex(boolean shuffleModeEnabled) { + return shuffleModeEnabled + ? fakeShuffleOrder.getFirstIndex() + : super.getFirstWindowIndex(/* shuffleModeEnabled= */ false); + } + + @Override + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex]; + window.set( + /* uid= */ windowDefinition.id, + windowDefinition.mediaItem, + manifests[windowIndex], + /* presentationStartTimeMs= */ C.TIME_UNSET, + /* windowStartTimeMs= */ windowDefinition.isLive + ? C.usToMs(windowDefinition.windowOffsetInFirstPeriodUs) + : C.TIME_UNSET, + /* elapsedRealtimeEpochOffsetMs= */ windowDefinition.isLive ? 0 : C.TIME_UNSET, + windowDefinition.isSeekable, + windowDefinition.isDynamic, + windowDefinition.isLive ? windowDefinition.mediaItem.liveConfiguration : null, + windowDefinition.defaultPositionUs, + windowDefinition.durationUs, + periodOffsets[windowIndex], + periodOffsets[windowIndex + 1] - 1, + windowDefinition.windowOffsetInFirstPeriodUs); + window.isPlaceholder = windowDefinition.isPlaceholder; + return window; + } + + @Override + public int getPeriodCount() { + return periodOffsets[periodOffsets.length - 1]; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + int windowIndex = Util.binarySearchFloor(periodOffsets, periodIndex, true, false); + int windowPeriodIndex = periodIndex - periodOffsets[windowIndex]; + TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex]; + Object id = setIds ? windowPeriodIndex : null; + Object uid = setIds ? Pair.create(windowDefinition.id, windowPeriodIndex) : null; + // Arbitrarily set period duration by distributing window duration equally among all periods. + long periodDurationUs = + windowDefinition.durationUs == C.TIME_UNSET + ? C.TIME_UNSET + : windowDefinition.durationUs / windowDefinition.periodCount; + long positionInWindowUs; + if (windowPeriodIndex == 0) { + if (windowDefinition.durationUs != C.TIME_UNSET) { + periodDurationUs += windowDefinition.windowOffsetInFirstPeriodUs; + } + positionInWindowUs = -windowDefinition.windowOffsetInFirstPeriodUs; + } else { + positionInWindowUs = periodDurationUs * windowPeriodIndex; + } + period.set( + id, + uid, + windowIndex, + periodDurationUs, + positionInWindowUs, + windowDefinition.adPlaybackState, + windowDefinition.isPlaceholder); + return period; + } + + @Override + public int getIndexOfPeriod(Object uid) { + for (int i = 0; i < getPeriodCount(); i++) { + if (getUidOfPeriod(i).equals(uid)) { + return i; + } + } + return C.INDEX_UNSET; + } + + @Override + public Object getUidOfPeriod(int periodIndex) { + Assertions.checkIndex(periodIndex, 0, getPeriodCount()); + int windowIndex = + Util.binarySearchFloor( + periodOffsets, periodIndex, /* inclusive= */ true, /* stayInBounds= */ false); + int windowPeriodIndex = periodIndex - periodOffsets[windowIndex]; + TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex]; + return Pair.create(windowDefinition.id, windowPeriodIndex); + } + + private static TimelineWindowDefinition[] createDefaultWindowDefinitions(int windowCount) { + TimelineWindowDefinition[] windowDefinitions = new TimelineWindowDefinition[windowCount]; + for (int i = 0; i < windowCount; i++) { + windowDefinitions[i] = new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ i); + } + return windowDefinitions; + } + +} diff --git a/playkit/src/main/java/com/kaltura/testhelper/TestUtils.kt b/playkit/src/main/java/com/kaltura/testhelper/TestUtils.kt new file mode 100644 index 000000000..07acffb89 --- /dev/null +++ b/playkit/src/main/java/com/kaltura/testhelper/TestUtils.kt @@ -0,0 +1,259 @@ +package com.kaltura.testhelper + +import android.content.Context +import android.net.Uri +import com.kaltura.android.exoplayer2.C +import com.kaltura.android.exoplayer2.Format +import com.kaltura.android.exoplayer2.metadata.Metadata +import com.kaltura.android.exoplayer2.source.TrackGroup +import com.kaltura.android.exoplayer2.source.TrackGroupArray +import com.kaltura.android.exoplayer2.source.dash.manifest.AdaptationSet +import com.kaltura.android.exoplayer2.source.dash.manifest.DashManifest +import com.kaltura.android.exoplayer2.source.dash.manifest.DashManifestParser +import com.kaltura.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist +import com.kaltura.android.exoplayer2.source.hls.playlist.HlsPlaylistParser +import com.kaltura.android.exoplayer2.util.MimeTypes +import com.kaltura.android.exoplayer2.util.Util +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.util.* +import kotlin.collections.ArrayList + +open class TestUtils { + companion object { + fun parseLocalDashManifest(context: Context, fileName: String): DashManifest { + val inputStream: InputStream = getInputStream(context, fileName) + return DashManifestParser().parse(Uri.EMPTY, inputStream) + } + + fun parseLocalHlsManifest(context: Context, fileName: String): HlsMasterPlaylist { + val inputStream: InputStream = getInputStream(context, fileName) + return (HlsPlaylistParser().parse(Uri.EMPTY, inputStream)) as HlsMasterPlaylist + } + + fun getManifestString(context: Context, fileName: String) : String { + val inputStream = getInputStream(context, fileName) + val bufferedReader = BufferedReader(InputStreamReader(inputStream)) + val manifestString = StringBuilder() + bufferedReader.useLines { lines -> lines.forEach { manifestString.append(it)} } + return manifestString.toString() + } + + @Throws(IOException::class) + fun getInputStream(context: Context, fileName: String): InputStream { + return context.resources.assets.open(fileName) + } + + @Throws(IOException::class) + fun getTrackGroupArrayFromDashManifest(context: Context, fileName: String): TrackGroupArray { + val dashManifest: DashManifest = parseLocalDashManifest(context, fileName) + val adaptationSets: List = dashManifest.getPeriod(0).adaptationSets + + val trackGroups: ArrayList = arrayListOf() + + for ((adaptationIndex, adaptationValue) in adaptationSets.withIndex()) { + val format: ArrayList = arrayListOf() + for ((representationIndex, representationValue) in adaptationValue.representations.withIndex()) { + format.add(adaptationSets[adaptationIndex].representations[representationIndex].format) + } + trackGroups.add(TrackGroup(*format.toTypedArray())) + } + + return TrackGroupArray(*trackGroups.toTypedArray()) + } + + @Throws(IOException::class) + fun getTrackGroupArrayFromHlsManifest(context: Context, fileName: String): TrackGroupArray { + val hlsMasterPlaylist = parseLocalHlsManifest(context, fileName) + var trackGroups: ArrayList = arrayListOf() + //MimeTypes.getMediaMimeType(Util.getCodecsOfType(trackGroups.get(2).getFormat(0).codecs, C.TRACK_TYPE_VIDEO)) + trackGroups = extractHlsVideoTracks(hlsMasterPlaylist, trackGroups) + trackGroups = extractHlsAudioTracks(hlsMasterPlaylist, trackGroups) + + if (hlsMasterPlaylist.subtitles.isNotEmpty()) { + for ((subtitlesIndex, subtitlesValue) in hlsMasterPlaylist.subtitles.withIndex()) { + trackGroups.add(TrackGroup(subtitlesValue.format)) + } + } + + return TrackGroupArray(*trackGroups.toTypedArray()) + } + + private fun deriveVideoFormat(variantFormat: Format): Format { + val codecs: String? = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO) + val sampleMimeType: String? = MimeTypes.getMediaMimeType(codecs) + return Format.Builder() + .setId(variantFormat.id) + .setLabel(variantFormat.label) + .setContainerMimeType(variantFormat.containerMimeType) + .setSampleMimeType(sampleMimeType ?: "video/avc") + .setCodecs(codecs) + .setMetadata(variantFormat.metadata) + .setAverageBitrate(variantFormat.averageBitrate) + .setPeakBitrate(variantFormat.peakBitrate) + .setWidth(variantFormat.width) + .setHeight(variantFormat.height) + .setFrameRate(variantFormat.frameRate) + .setSelectionFlags(variantFormat.selectionFlags) + .setRoleFlags(variantFormat.roleFlags) + .build() + } + + private fun deriveAudioFormat(variantFormat: Format, mediaTagFormat: Format?, isPrimaryTrackInVariant: Boolean): Format { + val codecs: String? + val metadata: Metadata? + var channelCount = Format.NO_VALUE + var selectionFlags = 0 + var roleFlags = 0 + var language: String? = null + var label: String? = null + if (mediaTagFormat != null) { + codecs = mediaTagFormat.codecs + metadata = mediaTagFormat.metadata + channelCount = mediaTagFormat.channelCount + selectionFlags = mediaTagFormat.selectionFlags + roleFlags = mediaTagFormat.roleFlags + language = mediaTagFormat.language + label = mediaTagFormat.label + } else { + codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_AUDIO) + metadata = variantFormat.metadata + if (isPrimaryTrackInVariant) { + channelCount = variantFormat.channelCount + selectionFlags = variantFormat.selectionFlags + roleFlags = variantFormat.roleFlags + language = variantFormat.language + label = variantFormat.label + } + } + val sampleMimeType: String? = MimeTypes.getMediaMimeType(codecs) + val averageBitrate = if (isPrimaryTrackInVariant) variantFormat.averageBitrate else Format.NO_VALUE + val peakBitrate = if (isPrimaryTrackInVariant) variantFormat.peakBitrate else Format.NO_VALUE + return Format.Builder() + .setId(variantFormat.id) + .setLabel(label) + .setContainerMimeType(variantFormat.containerMimeType) + .setSampleMimeType(sampleMimeType ?: "audio/mp4") + .setCodecs(codecs) + .setMetadata(metadata) + .setAverageBitrate(averageBitrate) + .setPeakBitrate(peakBitrate) + .setChannelCount(channelCount) + .setSelectionFlags(selectionFlags) + .setRoleFlags(roleFlags) + .setLanguage(language) + .build() + } + + private fun extractHlsVideoTracks(masterPlaylist: HlsMasterPlaylist, trackGroups: ArrayList): ArrayList { + val variantTypes = IntArray(masterPlaylist.variants.size) + var videoVariantCount = 0 + var audioVariantCount = 0 + for (i in 0 until masterPlaylist.variants.size) { + val variant: HlsMasterPlaylist.Variant = masterPlaylist.variants.get(i) + val format: Format = variant.format + if (format.height > 0 || Util.getCodecsOfType(format.codecs, C.TRACK_TYPE_VIDEO) != null) { + variantTypes[i] = C.TRACK_TYPE_VIDEO + videoVariantCount++ + } else if (Util.getCodecsOfType(format.codecs, C.TRACK_TYPE_AUDIO) != null) { + variantTypes[i] = C.TRACK_TYPE_AUDIO + audioVariantCount++ + } else { + variantTypes[i] = C.TRACK_TYPE_UNKNOWN + } + } + var useVideoVariantsOnly = false + var useNonAudioVariantsOnly = false + var selectedVariantsCount = variantTypes.size + if (videoVariantCount > 0) { + // We've identified some variants as definitely containing video. Assume variants within the + // master playlist are marked consistently, and hence that we have the full set. Filter out + // any other variants, which are likely to be audio only. + useVideoVariantsOnly = true + selectedVariantsCount = videoVariantCount + } else if (audioVariantCount < variantTypes.size) { + // We've identified some variants, but not all, as being audio only. Filter them out to leave + // the remaining variants, which are likely to contain video. + useNonAudioVariantsOnly = true + selectedVariantsCount = variantTypes.size - audioVariantCount + } + val selectedPlaylistUrls = arrayOfNulls(selectedVariantsCount) + val selectedPlaylistFormats = arrayOfNulls(selectedVariantsCount) + val selectedVariantIndices = IntArray(selectedVariantsCount) + var outIndex = 0 + for (i in 0 until masterPlaylist.variants.size) { + if ((!useVideoVariantsOnly || variantTypes[i] == C.TRACK_TYPE_VIDEO) + && (!useNonAudioVariantsOnly || variantTypes[i] != C.TRACK_TYPE_AUDIO)) { + val variant: HlsMasterPlaylist.Variant = masterPlaylist.variants.get(i) + selectedPlaylistUrls[outIndex] = variant.url + selectedPlaylistFormats[outIndex] = variant.format + selectedVariantIndices[outIndex++] = i + } + } + val codecs = selectedPlaylistFormats[0]!!.codecs + val numberOfVideoCodecs = Util.getCodecCountOfType(codecs, C.TRACK_TYPE_VIDEO) + val numberOfAudioCodecs = Util.getCodecCountOfType(codecs, C.TRACK_TYPE_AUDIO) + val codecsStringAllowsChunklessPreparation = numberOfAudioCodecs <= 1 && numberOfVideoCodecs <= 1 && numberOfAudioCodecs + numberOfVideoCodecs > 0 + // if (codecsStringAllowsChunklessPreparation) { + if (numberOfVideoCodecs > 0) { + val videoFormats = ArrayList() + for (index in 0 until selectedVariantsCount) { + videoFormats.add(deriveVideoFormat(selectedPlaylistFormats[index]!!)) + } + trackGroups.add(TrackGroup(*videoFormats.toTypedArray())) + if (numberOfAudioCodecs > 0 + && (masterPlaylist.muxedAudioFormat != null || masterPlaylist.audios.isEmpty())) { + trackGroups.add(TrackGroup(deriveAudioFormat(selectedPlaylistFormats[0]!!, masterPlaylist.muxedAudioFormat, false))) + } + masterPlaylist.muxedCaptionFormats?.let { ccFormats -> + for (i in ccFormats.indices) { + trackGroups.add(TrackGroup(ccFormats[i])) + } + } + } else /* numberOfAudioCodecs > 0 */ { + // Variants only contain audio. + val audioFormats = ArrayList() + for (index in 0 until selectedVariantsCount) { + audioFormats.add(deriveAudioFormat(selectedPlaylistFormats[index]!!, masterPlaylist.muxedAudioFormat, true)) + } + trackGroups.add(TrackGroup(*audioFormats.toTypedArray())) + } + val id3TrackGroup = TrackGroup( + Format.Builder() + .setId("ID3") + .setSampleMimeType(MimeTypes.APPLICATION_ID3) + .build()) + trackGroups.add(id3TrackGroup) + // } + + return trackGroups + } + + private fun extractHlsAudioTracks(masterPlaylist: HlsMasterPlaylist, trackGroups: ArrayList): ArrayList { + val scratchPlaylistFormats = ArrayList( /* initialCapacity= */masterPlaylist.audios.size) + val alreadyGroupedNames = HashSet() + for (renditionByNameIndex in masterPlaylist.audios.indices) { + val name: String = masterPlaylist.audios[renditionByNameIndex].name + if (!alreadyGroupedNames.add(name)) { + // This name already has a corresponding group. + continue + } + scratchPlaylistFormats.clear() + // Group all renditions with matching name. + for (renditionIndex in masterPlaylist.audios.indices) { + if (Util.areEqual(name, masterPlaylist.audios[renditionIndex].name)) { + val rendition: HlsMasterPlaylist.Rendition = masterPlaylist.audios[renditionIndex] + scratchPlaylistFormats.add(rendition.format) + } + } + } + + if (scratchPlaylistFormats.size > 0) { + trackGroups.add(TrackGroup(*scratchPlaylistFormats.toTypedArray())) + } + return trackGroups + } + } +} diff --git a/playkit/src/test/assets/testdata/m3u8/sample_hls_abr_test b/playkit/src/test/assets/testdata/m3u8/sample_hls_abr_test new file mode 100644 index 000000000..260d7738e --- /dev/null +++ b/playkit/src/test/assets/testdata/m3u8/sample_hls_abr_test @@ -0,0 +1,13 @@ +#EXTM3U + +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio0",NAME="Hindi",LANGUAGE="hi",AUTOSELECT=YES,DEFAULT=YES,CHANNELS="1",URI="https://exmple.net/fpshls/p/sp/pqwert1_pxycp3t2/name/a.mp4/index.m3u8/index-a1.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio0",NAME="English",LANGUAGE="en",AUTOSELECT=NO,DEFAULT=NO,CHANNELS="1",URI="https://exmple.net/fpshls/p/sp/pqwert1_w62mxbkz/name/a.mp4/index.m3u8/index-a1.m3u8" + +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=769255,RESOLUTION=320x142,FRAME-RATE=25.000,CODECS="avc1.42c01e,mp4a.40.2",AUDIO="audio0" +https://exmple.net/fpshls/p/sp/pqwert1_h1q0wy1u/name/a.mp4/index.m3u8/index-v1-a1.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1774254,RESOLUTION=854x380,FRAME-RATE=25.000,CODECS="avc1.42c01e,mp4a.40.2",AUDIO="audio0" +https://exmple.net/fpshls/p/sp/pqwert1_uo5tcmjd/name/a.mp4/index.m3u8/index-v1-a1.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=7203938,RESOLUTION=1280x570,FRAME-RATE=25.000,CODECS="avc1.42c01e,mp4a.40.2",AUDIO="audio0" +https://exmple.net/fpshls/p/sp/pqwert1_h8c2v7qh/name/a.mp4/index.m3u8/index-v1-a1.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=18316946,RESOLUTION=1920x856,FRAME-RATE=25.000,CODECS="avc1.42c01e,mp4a.40.2",AUDIO="audio0" +https://exmple.net/fpshls/p/sp/pqwert1_mmf5jo25/name/a.mp4/index.m3u8/index-v1-a1.m3u8 diff --git a/playkit/src/test/assets/testdata/m3u8/sample_hls_dolby_vod b/playkit/src/test/assets/testdata/m3u8/sample_hls_dolby_vod new file mode 100644 index 000000000..844d9d5d5 --- /dev/null +++ b/playkit/src/test/assets/testdata/m3u8/sample_hls_dolby_vod @@ -0,0 +1,56 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS + +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="ec3_stereo",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2",URI="audio_ddp_2ch_eng/prog_index.m3u8" + +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_stereo",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2",URI="audio_aac_2ch_eng/prog_index.m3u8" + + +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=6303166,BANDWIDTH=11265828,CODECS="avc1.640028,ec-3",RESOLUTION=1920x1080,FRAME-RATE=25.000,SUBTITLES="subs-ec3-20",CLOSED-CAPTIONS=NONE,AUDIO="ec3_stereo" +video_6000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=4750392,BANDWIDTH=7979513,CODECS="avc1.640028,ec-3",RESOLUTION=1920x1080,FRAME-RATE=25.000,SUBTITLES="subs-ec3-20",CLOSED-CAPTIONS=NONE,AUDIO="ec3_stereo" +video_4500k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=3202998,BANDWIDTH=5134820,CODECS="avc1.4d001f,ec-3",RESOLUTION=1280x720,FRAME-RATE=25.000,SUBTITLES="subs-ec3-20",CLOSED-CAPTIONS=NONE,AUDIO="ec3_stereo" +video_3000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=2196234,BANDWIDTH=3305648,CODECS="avc1.4d001f,ec-3",RESOLUTION=960x540,FRAME-RATE=25.000,SUBTITLES="subs-ec3-20",CLOSED-CAPTIONS=NONE,AUDIO="ec3_stereo" +video_2000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=1259726,BANDWIDTH=1918208,CODECS="avc1.4d001e,ec-3",RESOLUTION=768x432,FRAME-RATE=25.000,SUBTITLES="subs-ec3-20",CLOSED-CAPTIONS=NONE,AUDIO="ec3_stereo" +video_1000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=884732,BANDWIDTH=1270234,CODECS="avc1.42001e,ec-3",RESOLUTION=640x360,FRAME-RATE=25.000,SUBTITLES="subs-ec3-20",CLOSED-CAPTIONS=NONE,AUDIO="ec3_stereo" +video_730k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=503255,BANDWIDTH=669126,CODECS="avc1.42e015,ec-3",RESOLUTION=480x270,FRAME-RATE=25.000,SUBTITLES="subs-ec3-20",CLOSED-CAPTIONS=NONE,AUDIO="ec3_stereo" +video_365k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=229236,BANDWIDTH=271811,CODECS="avc1.42e00b,ec-3",RESOLUTION=416x234,FRAME-RATE=4.000,SUBTITLES="subs-ec3-20",CLOSED-CAPTIONS=NONE,AUDIO="ec3_stereo" +video_192k/prog_index.m3u8 + +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=6305791,BANDWIDTH=11204864,CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=25.000,SUBTITLES="subs-aac",CLOSED-CAPTIONS=NONE,AUDIO="aac_stereo" +video_6000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=4753017,BANDWIDTH=7981552,CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=25.000,SUBTITLES="subs-aac",CLOSED-CAPTIONS=NONE,AUDIO="aac_stereo" +video_4500k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=3205623,BANDWIDTH=5137859,CODECS="avc1.4d001f,mp4a.40.2",RESOLUTION=1280x720,FRAME-RATE=25.000,SUBTITLES="subs-aac",CLOSED-CAPTIONS=NONE,AUDIO="aac_stereo" +video_3000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=2198859,BANDWIDTH=3308768,CODECS="avc1.4d001f,mp4a.40.2",RESOLUTION=960x540,FRAME-RATE=25.000,SUBTITLES="subs-aac",CLOSED-CAPTIONS=NONE,AUDIO="aac_stereo" +video_2000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=1262351,BANDWIDTH=1921247,CODECS="avc1.4d001e,mp4a.40.2",RESOLUTION=768x432,FRAME-RATE=25.000,SUBTITLES="subs-aac",CLOSED-CAPTIONS=NONE,AUDIO="aac_stereo" +video_1000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=887357,BANDWIDTH=1273273,CODECS="avc1.42001e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=25.000,SUBTITLES="subs-aac",CLOSED-CAPTIONS=NONE,AUDIO="aac_stereo" +video_730k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=505880,BANDWIDTH=672165,CODECS="avc1.42e015,mp4a.40.2",RESOLUTION=480x270,FRAME-RATE=25.000,SUBTITLES="subs-aac",CLOSED-CAPTIONS=NONE,AUDIO="aac_stereo" +video_365k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=231861,BANDWIDTH=274850,CODECS="avc1.42e00b,mp4a.40.2",RESOLUTION=416x234,FRAME-RATE=4.000,SUBTITLES="subs-aac",CLOSED-CAPTIONS=NONE,AUDIO="aac_stereo" +video_192k/prog_index.m3u8 + + +#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=427577,BANDWIDTH=1673200,CODECS="avc1.640028",RESOLUTION=1920x1080,URI="video_6000k/iframe_index.m3u8" +#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=339923,BANDWIDTH=1304431,CODECS="avc1.640028",RESOLUTION=1920x1080,URI="video_4500k/iframe_index.m3u8" +#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=250827,BANDWIDTH=973262,CODECS="avc1.4d001f",RESOLUTION=1280x720,URI="video_3000k/iframe_index.m3u8" +#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=172544,BANDWIDTH=620400,CODECS="avc1.4d001f",RESOLUTION=960x540,URI="video_2000k/iframe_index.m3u8" +#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=107738,BANDWIDTH=397693,CODECS="avc1.4d001e",RESOLUTION=768x432,URI="video_1000k/iframe_index.m3u8" +#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=69062,BANDWIDTH=228493,CODECS="avc1.42001e",RESOLUTION=640x360,URI="video_730k/iframe_index.m3u8" +#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=59513,BANDWIDTH=157562,CODECS="avc1.42e015",RESOLUTION=480x270,URI="video_365k/iframe_index.m3u8" +#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=39939,BANDWIDTH=81968,CODECS="avc1.42e00b",RESOLUTION=416x234,URI="video_192k/iframe_index.m3u8" + + +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs-ec3-20",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="subtitles/ec3_20_eng/subs.m3u8" +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs-aac",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="subtitles/aac_eng/subs.m3u8" \ No newline at end of file diff --git a/playkit/src/test/assets/testdata/m3u8/sample_hls_harold b/playkit/src/test/assets/testdata/m3u8/sample_hls_harold new file mode 100644 index 000000000..5186db5ca --- /dev/null +++ b/playkit/src/test/assets/testdata/m3u8/sample_hls_harold @@ -0,0 +1,12 @@ +#EXTM3U +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="en",URI="http://cfvod.kaltura.com/api_v3/index.php/service/caption_captionasset/action/serveWebVTT/captionAssetId/1_2z8njt8w/segmentDuration/300/ks/MWZhNWY3ZjlhOTU4ZWMzYzcwM2MxNmZjYTFhZTQyZjFhYjdmNWEyZXwyNDMzNDI7MjQzMzQyOzE2MjIxODM1ODk7MDsxNjIyMDk3MTg5LjY4MTc7O2NhcHRpb246MF91a2ExbXNnNCxkaXNhYmxlZW50aXRsZW1lbnRmb3JlbnRyeTowX3VrYTFtc2c0LHVyaXJlc3RyaWN0Oi9hcGlfdjMvaW5kZXgucGhwL3NlcnZpY2UvY2FwdGlvbl9jYXB0aW9uYXNzZXQvYWN0aW9uL3NlcnZlV2ViVlRUKjs7/a.m3u8" +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="French",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="fr",URI="http://cfvod.kaltura.com/api_v3/index.php/service/caption_captionasset/action/serveWebVTT/captionAssetId/1_gtwqks49/segmentDuration/300/ks/NjQ4YTdlMDMzMjhjOTBhYjdmZGQ5MzExYzUyNDJlZGFmNzMxYmJiM3wyNDMzNDI7MjQzMzQyOzE2MjIxODM1ODk7MDsxNjIyMDk3MTg5LjY4Mjc7O2NhcHRpb246MF91a2ExbXNnNCxkaXNhYmxlZW50aXRsZW1lbnRmb3JlbnRyeTowX3VrYTFtc2c0LHVyaXJlc3RyaWN0Oi9hcGlfdjMvaW5kZXgucGhwL3NlcnZpY2UvY2FwdGlvbl9jYXB0aW9uYXNzZXQvYWN0aW9uL3NlcnZlV2ViVlRUKjs7/a.m3u8" +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Spanish",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="es",URI="http://cfvod.kaltura.com/api_v3/index.php/service/caption_captionasset/action/serveWebVTT/captionAssetId/1_hjfu39fx/segmentDuration/300/ks/Mjc2MGZiZDZhNjE1YzI3YTFkMjViODFiMmVjZjRlZDA2ZjY3ZWMzMnwyNDMzNDI7MjQzMzQyOzE2MjIxODM1ODk7MDsxNjIyMDk3MTg5LjY4MzU7O2NhcHRpb246MF91a2ExbXNnNCxkaXNhYmxlZW50aXRsZW1lbnRmb3JlbnRyeTowX3VrYTFtc2c0LHVyaXJlc3RyaWN0Oi9hcGlfdjMvaW5kZXgucGhwL3NlcnZpY2UvY2FwdGlvbl9jYXB0aW9uYXNzZXQvYWN0aW9uL3NlcnZlV2ViVlRUKjs7/a.m3u8" +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Arabic",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="ar",URI="http://cfvod.kaltura.com/api_v3/index.php/service/caption_captionasset/action/serveWebVTT/captionAssetId/1_qjgojnhs/segmentDuration/300/ks/M2RmNWViOTkxYjIwZjcxOGVhOGM2ZTQxZDY1ZTBmOGJhYzVmYjYxYnwyNDMzNDI7MjQzMzQyOzE2MjIxODM1ODk7MDsxNjIyMDk3MTg5LjY4NDM7O2NhcHRpb246MF91a2ExbXNnNCxkaXNhYmxlZW50aXRsZW1lbnRmb3JlbnRyeTowX3VrYTFtc2c0LHVyaXJlc3RyaWN0Oi9hcGlfdjMvaW5kZXgucGhwL3NlcnZpY2UvY2FwdGlvbl9jYXB0aW9uYXNzZXQvYWN0aW9uL3NlcnZlV2ViVlRUKjs7/a.m3u8" +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Dutch",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="nl",URI="http://cfvod.kaltura.com/api_v3/index.php/service/caption_captionasset/action/serveWebVTT/captionAssetId/1_jf4xx36w/segmentDuration/300/ks/MDU2MDY0MTk3YWYzYTlhN2Y2NGM2NWUzZjI4NWQ5ZDZjNDNjZGRmMnwyNDMzNDI7MjQzMzQyOzE2MjIxODM1ODk7MDsxNjIyMDk3MTg5LjY4NTI7O2NhcHRpb246MF91a2ExbXNnNCxkaXNhYmxlZW50aXRsZW1lbnRmb3JlbnRyeTowX3VrYTFtc2c0LHVyaXJlc3RyaWN0Oi9hcGlfdjMvaW5kZXgucGhwL3NlcnZpY2UvY2FwdGlvbl9jYXB0aW9uYXNzZXQvYWN0aW9uL3NlcnZlV2ViVlRUKjs7/a.m3u8" +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Japanese",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="ja",URI="http://cfvod.kaltura.com/api_v3/index.php/service/caption_captionasset/action/serveWebVTT/captionAssetId/1_flsdft0e/segmentDuration/300/ks/ZGE0YTkzYTM4ZWJkYTQ4ODVlODUwYzFlOWQ2ZDU2NmE5NTgxZjRhNHwyNDMzNDI7MjQzMzQyOzE2MjIxODM1ODk7MDsxNjIyMDk3MTg5LjY4Njs7Y2FwdGlvbjowX3VrYTFtc2c0LGRpc2FibGVlbnRpdGxlbWVudGZvcmVudHJ5OjBfdWthMW1zZzQsdXJpcmVzdHJpY3Q6L2FwaV92My9pbmRleC5waHAvc2VydmljZS9jYXB0aW9uX2NhcHRpb25hc3NldC9hY3Rpb24vc2VydmVXZWJWVFQqOzs=/a.m3u8" +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Russian",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="ru",URI="http://cfvod.kaltura.com/api_v3/index.php/service/caption_captionasset/action/serveWebVTT/captionAssetId/1_w6zxhvm7/segmentDuration/300/ks/ZWE1MWIwOTJiMDUyOWZiZDkzYTIxNGQ0MjRkNDhjZmM1YjFkYTIxNXwyNDMzNDI7MjQzMzQyOzE2MjIxODM1ODk7MDsxNjIyMDk3MTg5LjY4Njg7O2NhcHRpb246MF91a2ExbXNnNCxkaXNhYmxlZW50aXRsZW1lbnRmb3JlbnRyeTowX3VrYTFtc2c0LHVyaXJlc3RyaWN0Oi9hcGlfdjMvaW5kZXgucGhwL3NlcnZpY2UvY2FwdGlvbl9jYXB0aW9uYXNzZXQvYWN0aW9uL3NlcnZlV2ViVlRUKjs7/a.m3u8" +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=437304,RESOLUTION=480x352,SUBTITLES="subs" +http://cfvod.kaltura.com/scf/hls/p/243342/sp/24334200/serveFlavor/entryId/0_uka1msg4/v/1/pv/2/flavorId/1_vqhfu6uy/name/a.mp4/index.m3u8?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cDovL2Nmdm9kLmthbHR1cmEuY29tL3NjZi9obHMvcC8yNDMzNDIvc3AvMjQzMzQyMDAvc2VydmVGbGF2b3IvZW50cnlJZC8wX3VrYTFtc2c0L3YvMS9wdi8yKiIsIkNvbmRpdGlvbiI6eyJEYXRlTGVzc1RoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTYyMjE4MzU4OX19fV19&Signature=glnACGQWH2q6QmRf5ZoK0RW455PDPjG2Nu3Y7Bsuas9Alw-I4DgEshcUCyLKjRpqAS4O0-AbVk4ILIc7wFgsQNU4T2NXGgQEG0UtcHyv2lCb6P5ekaDEXkrTLzUYcbXD7GP5oe41usBq11cXoRhEwRXGvOWcFL52CyILxhr~mTYhOMOvKqjH5ovgHClD1D-WJAkUlgoXHa5p~vhcf0XU0~JEAUuvuz9aEsOcjy3-fC7D5gq566V57f17CbPUND8q~a7lRPS3JN0zm5rsMAS1etluu8v33-0Qe0i5HmdslimCxuVN-ZKKdwODTh~I7UDc~NiZcQE6HCvvmRxj2h4rlQ__&Key-Pair-Id=APKAJT6QIWSKVYK3V34A +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=850577,RESOLUTION=480x352,SUBTITLES="subs" +http://cfvod.kaltura.com/scf/hls/p/243342/sp/24334200/serveFlavor/entryId/0_uka1msg4/v/1/pv/2/flavorId/1_80sohj7p/name/a.mp4/index.m3u8?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cDovL2Nmdm9kLmthbHR1cmEuY29tL3NjZi9obHMvcC8yNDMzNDIvc3AvMjQzMzQyMDAvc2VydmVGbGF2b3IvZW50cnlJZC8wX3VrYTFtc2c0L3YvMS9wdi8yKiIsIkNvbmRpdGlvbiI6eyJEYXRlTGVzc1RoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTYyMjE4MzU4OX19fV19&Signature=glnACGQWH2q6QmRf5ZoK0RW455PDPjG2Nu3Y7Bsuas9Alw-I4DgEshcUCyLKjRpqAS4O0-AbVk4ILIc7wFgsQNU4T2NXGgQEG0UtcHyv2lCb6P5ekaDEXkrTLzUYcbXD7GP5oe41usBq11cXoRhEwRXGvOWcFL52CyILxhr~mTYhOMOvKqjH5ovgHClD1D-WJAkUlgoXHa5p~vhcf0XU0~JEAUuvuz9aEsOcjy3-fC7D5gq566V57f17CbPUND8q~a7lRPS3JN0zm5rsMAS1etluu8v33-0Qe0i5HmdslimCxuVN-ZKKdwODTh~I7UDc~NiZcQE6HCvvmRxj2h4rlQ__&Key-Pair-Id=APKAJT6QIWSKVYK3V34A diff --git a/playkit/src/test/assets/testdata/m3u8/sample_hls_live_low_latency_master b/playkit/src/test/assets/testdata/m3u8/sample_hls_live_low_latency_master new file mode 100644 index 000000000..e595fcace --- /dev/null +++ b/playkit/src/test/assets/testdata/m3u8/sample_hls_live_low_latency_master @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-INDEPENDENT-SEGMENTS + +#EXT-X-STREAM-INF:BANDWIDTH=2000000,CODECS="avc1.640028,mp4a.40.2" +media0/playlist.m3u8 diff --git a/playkit/src/test/assets/testdata/m3u8/sample_hls_multi_codec b/playkit/src/test/assets/testdata/m3u8/sample_hls_multi_codec new file mode 100644 index 000000000..2e6e415db --- /dev/null +++ b/playkit/src/test/assets/testdata/m3u8/sample_hls_multi_codec @@ -0,0 +1,14 @@ +#EXTM3U +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-VERSION:6 + +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio_128",NAME="audio_128.m3u8",LANGUAGE="en",URI="audio_128.m3u8" + +#EXT-X-STREAM-INF:BANDWIDTH=2101985,AVERAGE-BANDWIDTH=1796254,CODECS="avc1.4D401F,mp4a.40.2",RESOLUTION=854x480,AUDIO="audio_128" +video_h265_480p_1750.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2739681,AVERAGE-BANDWIDTH=2372431,CODECS="avc1.4D401F,mp4a.40.2",RESOLUTION=1280x720,AUDIO="audio_128" +Video_h265_720p_2350.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2049427,AVERAGE-BANDWIDTH=1824322,CODECS="hev1.1.6.L90.90,mp4a.40.2",RESOLUTION=854x480,AUDIO="audio_128" +video_h264_480p_1750.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2706785,AVERAGE-BANDWIDTH=2421640,CODECS="hev1.1.6.L93.90,mp4a.40.2",RESOLUTION=1280x720,AUDIO="audio_128" +video_h264_720p_2350.m3u8 \ No newline at end of file diff --git a/playkit/src/test/assets/testdata/m3u8/sample_hls_multi_lang b/playkit/src/test/assets/testdata/m3u8/sample_hls_multi_lang new file mode 100644 index 000000000..c590aad62 --- /dev/null +++ b/playkit/src/test/assets/testdata/m3u8/sample_hls_multi_lang @@ -0,0 +1,104 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS + +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="ec3",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="6",URI="audio_ddp_51_eng/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="ec3",NAME="Español",LANGUAGE="es",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="6",URI="audio_ddp_51_spa/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="ec3",NAME="中文",LANGUAGE="zh",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="6",URI="audio_ddp_51_chi/prog_index.m3u8" + +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="ac3",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="6",URI="audio_dd_51_eng/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="ac3",NAME="Español",LANGUAGE="es",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="6",URI="audio_dd_51_spa/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="ac3",NAME="中文",LANGUAGE="zh",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="6",URI="audio_dd_51_chi/prog_index.m3u8" + +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="ec3_stereo",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2",URI="audio_ddp_2ch_eng/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="ec3_stereo",NAME="Español",LANGUAGE="es",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="2",URI="audio_ddp_2ch_spa/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="ec3_stereo",NAME="中文",LANGUAGE="zh",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="2",URI="audio_ddp_2ch_chi/prog_index.m3u8" + +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2",URI="audio_aac_2ch_eng/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Español",LANGUAGE="es",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="2",URI="audio_aac_2ch_spa/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="中文",LANGUAGE="zh",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="2",URI="audio_aac_2ch_chi/prog_index.m3u8" + + +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=6303166,BANDWIDTH=11265828,CODECS="avc1.640028,ec-3",RESOLUTION=1920x1080,FRAME-RATE=25.000,SUBTITLES="subs-ec3-20",CLOSED-CAPTIONS=NONE,AUDIO="ec3_stereo" +video_6000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=4750392,BANDWIDTH=7979513,CODECS="avc1.640028,ec-3",RESOLUTION=1920x1080,FRAME-RATE=25.000,SUBTITLES="subs-ec3-20",CLOSED-CAPTIONS=NONE,AUDIO="ec3_stereo" +video_4500k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=3202998,BANDWIDTH=5134820,CODECS="avc1.4d001f,ec-3",RESOLUTION=1280x720,FRAME-RATE=25.000,SUBTITLES="subs-ec3-20",CLOSED-CAPTIONS=NONE,AUDIO="ec3_stereo" +video_3000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=2196234,BANDWIDTH=3305648,CODECS="avc1.4d001f,ec-3",RESOLUTION=960x540,FRAME-RATE=25.000,SUBTITLES="subs-ec3-20",CLOSED-CAPTIONS=NONE,AUDIO="ec3_stereo" +video_2000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=1259726,BANDWIDTH=1918208,CODECS="avc1.4d001e,ec-3",RESOLUTION=768x432,FRAME-RATE=25.000,SUBTITLES="subs-ec3-20",CLOSED-CAPTIONS=NONE,AUDIO="ec3_stereo" +video_1000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=884732,BANDWIDTH=1270234,CODECS="avc1.42001e,ec-3",RESOLUTION=640x360,FRAME-RATE=25.000,SUBTITLES="subs-ec3-20",CLOSED-CAPTIONS=NONE,AUDIO="ec3_stereo" +video_730k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=503255,BANDWIDTH=669126,CODECS="avc1.42e015,ec-3",RESOLUTION=480x270,FRAME-RATE=25.000,SUBTITLES="subs-ec3-20",CLOSED-CAPTIONS=NONE,AUDIO="ec3_stereo" +video_365k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=229236,BANDWIDTH=271811,CODECS="avc1.42e00b,ec-3",RESOLUTION=416x234,FRAME-RATE=4.000,SUBTITLES="subs-ec3-20",CLOSED-CAPTIONS=NONE,AUDIO="ec3_stereo" +video_192k/prog_index.m3u8 + +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=6305791,BANDWIDTH=11204864,CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=25.000,SUBTITLES="subs-aac",CLOSED-CAPTIONS=NONE,AUDIO="aac" +video_6000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=4753017,BANDWIDTH=7981552,CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=25.000,SUBTITLES="subs-aac",CLOSED-CAPTIONS=NONE,AUDIO="aac" +video_4500k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=3205623,BANDWIDTH=5137859,CODECS="avc1.4d001f,mp4a.40.2",RESOLUTION=1280x720,FRAME-RATE=25.000,SUBTITLES="subs-aac",CLOSED-CAPTIONS=NONE,AUDIO="aac" +video_3000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=2198859,BANDWIDTH=3308768,CODECS="avc1.4d001f,mp4a.40.2",RESOLUTION=960x540,FRAME-RATE=25.000,SUBTITLES="subs-aac",CLOSED-CAPTIONS=NONE,AUDIO="aac" +video_2000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=1262351,BANDWIDTH=1921247,CODECS="avc1.4d001e,mp4a.40.2",RESOLUTION=768x432,FRAME-RATE=25.000,SUBTITLES="subs-aac",CLOSED-CAPTIONS=NONE,AUDIO="aac" +video_1000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=887357,BANDWIDTH=1273273,CODECS="avc1.42001e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=25.000,SUBTITLES="subs-aac",CLOSED-CAPTIONS=NONE,AUDIO="aac" +video_730k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=505880,BANDWIDTH=672165,CODECS="avc1.42e015,mp4a.40.2",RESOLUTION=480x270,FRAME-RATE=25.000,SUBTITLES="subs-aac",CLOSED-CAPTIONS=NONE,AUDIO="aac" +video_365k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=231861,BANDWIDTH=274850,CODECS="avc1.42e00b,mp4a.40.2",RESOLUTION=416x234,FRAME-RATE=4.000,SUBTITLES="subs-aac",CLOSED-CAPTIONS=NONE,AUDIO="aac" +video_192k/prog_index.m3u8 + +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=6367166,BANDWIDTH=11265828,CODECS="avc1.640028,ec-3",RESOLUTION=1920x1080,FRAME-RATE=25.000,SUBTITLES="subs-ec3-51",CLOSED-CAPTIONS=NONE,AUDIO="ec3" +video_6000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=4814392,BANDWIDTH=8042516,CODECS="avc1.640028,ec-3",RESOLUTION=1920x1080,FRAME-RATE=25.000,SUBTITLES="subs-ec3-51",CLOSED-CAPTIONS=NONE,AUDIO="ec3" +video_4500k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=3266998,BANDWIDTH=5198823,CODECS="avc1.4d001f,ec-3",RESOLUTION=1280x720,FRAME-RATE=25.000,SUBTITLES="subs-ec3-51",CLOSED-CAPTIONS=NONE,AUDIO="ec3" +video_3000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=2260234,BANDWIDTH=3369651,CODECS="avc1.4d001f,ec-3",RESOLUTION=960x540,FRAME-RATE=25.000,SUBTITLES="subs-ec3-51",CLOSED-CAPTIONS=NONE,AUDIO="ec3" +video_2000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=1332726,BANDWIDTH=1982211,CODECS="avc1.4d001e,ec-3",RESOLUTION=768x432,FRAME-RATE=25.000,SUBTITLES="subs-ec3-51",CLOSED-CAPTIONS=NONE,AUDIO="ec3" +video_1000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=948732,BANDWIDTH=1334237,CODECS="avc1.42001e,ec-3",RESOLUTION=640x360,FRAME-RATE=25.000,SUBTITLES="subs-ec3-51",CLOSED-CAPTIONS=NONE,AUDIO="ec3" +video_730k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=567255,BANDWIDTH=733129,CODECS="avc1.42e015,ec-3",RESOLUTION=480x270,FRAME-RATE=25.000,SUBTITLES="subs-ec3-51",CLOSED-CAPTIONS=NONE,AUDIO="ec3" +video_365k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=293236,BANDWIDTH=335814,CODECS="avc1.42e00b,ec-3",RESOLUTION=416x234,FRAME-RATE=4.000,SUBTITLES="subs-ec3-51",CLOSED-CAPTIONS=NONE,AUDIO="ec3" +video_192k/prog_index.m3u8 + +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=6559166,BANDWIDTH=11457828,CODECS="avc1.640028,ac-3",RESOLUTION=1920x1080,FRAME-RATE=25.000,SUBTITLES="subs-ac3",CLOSED-CAPTIONS=NONE,AUDIO="ac3" +video_6000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=5006392,BANDWIDTH=8234516,CODECS="avc1.640028,ac-3",RESOLUTION=1920x1080,FRAME-RATE=25.000,SUBTITLES="subs-ac3",CLOSED-CAPTIONS=NONE,AUDIO="ac3" +video_4500k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=3458998,BANDWIDTH=5390823,CODECS="avc1.4d001f,ac-3",RESOLUTION=1280x720,FRAME-RATE=25.000,SUBTITLES="subs-ac3",CLOSED-CAPTIONS=NONE,AUDIO="ac3" +video_3000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=2452234,BANDWIDTH=3561651,CODECS="avc1.4d001f,ac-3",RESOLUTION=960x540,FRAME-RATE=25.000,SUBTITLES="subs-ac3",CLOSED-CAPTIONS=NONE,AUDIO="ac3" +video_2000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=1515726,BANDWIDTH=2174211,CODECS="avc1.4d001e,ac-3",RESOLUTION=768x432,FRAME-RATE=25.000,SUBTITLES="subs-ac3",CLOSED-CAPTIONS=NONE,AUDIO="ac3" +video_1000k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=1140732,BANDWIDTH=1526237,CODECS="avc1.42001e,ac-3",RESOLUTION=640x360,FRAME-RATE=25.000,SUBTITLES="subs-ac3",CLOSED-CAPTIONS=NONE,AUDIO="ac3" +video_730k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=759255,BANDWIDTH=925129,CODECS="avc1.42e015,ac-3",RESOLUTION=480x270,FRAME-RATE=25.000,SUBTITLES="subs-ac3",CLOSED-CAPTIONS=NONE,AUDIO="ac3" +video_365k/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=485236,BANDWIDTH=527814,CODECS="avc1.42e00b,ac-3",RESOLUTION=416x234,FRAME-RATE=4.000,SUBTITLES="subs-ac3",CLOSED-CAPTIONS=NONE,AUDIO="ac3" +video_192k/prog_index.m3u8 + +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs-ec3-20",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="subtitles/ec3_20_eng/subs.m3u8" +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs-ec3-20",NAME="English",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="chi",URI="subtitles/ec3_20_eng/subs.m3u8" +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs-ec3-20",NAME="English",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="spa",URI="subtitles/ec3_20_eng/subs.m3u8" + +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs-aac",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="subtitles/aac_eng/subs.m3u8" +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs-aac",NAME="English",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="chi",URI="subtitles/aac_eng/subs.m3u8" +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs-aac",NAME="English",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="spa",URI="subtitles/aac_eng/subs.m3u8" + +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs-ec3-51",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="subtitles/ec3_51_eng/subs.m3u8" +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs-ec3-51",NAME="English",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="chi",URI="subtitles/ec3_51_eng/subs.m3u8" +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs-ec3-51",NAME="English",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="spa",URI="subtitles/ec3_51_eng/subs.m3u8" + +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs-ac3",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="subtitles/ac3_51_eng/subs.m3u8" +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs-ac3",NAME="English",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="chi",URI="subtitles/ac3_51_eng/subs.m3u8" +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs-ac3",NAME="English",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="spa",URI="subtitles/ac3_51_eng/subs.m3u8" diff --git a/playkit/src/test/assets/testdata/m3u8/sample_hls_short_sintel b/playkit/src/test/assets/testdata/m3u8/sample_hls_short_sintel new file mode 100644 index 000000000..4109a9608 --- /dev/null +++ b/playkit/src/test/assets/testdata/m3u8/sample_hls_short_sintel @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=460259,RESOLUTION=480x256,FRAME-RATE=24.000,CODECS="avc1.42c01e,mp4a.40.2" +http://cfvod.kaltura.com/hls/p/2215841/sp/221584100/serveFlavor/entryId/1_9bwuo813/v/1/ev/1/flavorId/1_5j0bgx4v/name/a.mp4/index.m3u8/index-v1-a1.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=629957,RESOLUTION=480x256,FRAME-RATE=24.000,CODECS="avc1.4d401f,mp4a.40.2" +http://cfvod.kaltura.com/hls/p/2215841/sp/221584100/serveFlavor/entryId/1_9bwuo813/v/1/ev/1/flavorId/1_zj4vzg46/name/a.mp4/index.m3u8/index-v1-a1.m3u8 diff --git a/playkit/src/test/assets/testdata/m3u8/sample_hls_tears b/playkit/src/test/assets/testdata/m3u8/sample_hls_tears new file mode 100644 index 000000000..7e3eac5f9 --- /dev/null +++ b/playkit/src/test/assets/testdata/m3u8/sample_hls_tears @@ -0,0 +1,30 @@ +#EXTM3U +#EXT-X-VERSION:1 + +#EXT-X-STREAM-INF:BANDWIDTH=493000,CODECS="avc1.66.30",RESOLUTION=224x100,FRAME-RATE=24 +tears-of-steel-audio_eng=64008-video_eng=401000.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=932000,CODECS="avc1.66.30",RESOLUTION=448x200,FRAME-RATE=24 +tears-of-steel-audio_eng=128002-video_eng=751000.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=1197000,CODECS="avc1.77.31",RESOLUTION=784x350,FRAME-RATE=24 +tears-of-steel-audio_eng=128002-video_eng=1001000.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=1727000,CODECS="avc1.100.40",RESOLUTION=1680x750,FRAME-RATE=24,VIDEO-RANGE=SDR +tears-of-steel-audio_eng=128002-video_eng=1501000.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2468000,CODECS="avc1.100.40",RESOLUTION=1680x750,FRAME-RATE=24,VIDEO-RANGE=SDR +tears-of-steel-audio_eng=128002-video_eng=2200000.m3u8 + +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="ec3",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="6",URI="audio_ddp_51_eng/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="ec3",NAME="Español",LANGUAGE="es",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="6",URI="audio_ddp_51_spa/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="ec3",NAME="中文",LANGUAGE="zh",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="6",URI="audio_ddp_51_chi/prog_index.m3u8" + +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="ac3",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="6",URI="audio_dd_51_eng/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="ac3",NAME="Español",LANGUAGE="es",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="6",URI="audio_dd_51_spa/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="ac3",NAME="中文",LANGUAGE="zh",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="6",URI="audio_dd_51_chi/prog_index.m3u8" + +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="ec3_stereo",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2",URI="audio_ddp_2ch_eng/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="ec3_stereo",NAME="Español",LANGUAGE="es",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="2",URI="audio_ddp_2ch_spa/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="ec3_stereo",NAME="中文",LANGUAGE="zh",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="2",URI="audio_ddp_2ch_chi/prog_index.m3u8" + +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2",URI="audio_aac_2ch_eng/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Español",LANGUAGE="es",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="2",URI="audio_aac_2ch_spa/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="中文",LANGUAGE="zh",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="2",URI="audio_aac_2ch_chi/prog_index.m3u8" + diff --git a/playkit/src/test/assets/testdata/mpd/sample_dash_audio_only b/playkit/src/test/assets/testdata/mpd/sample_dash_audio_only new file mode 100644 index 000000000..48847f57f --- /dev/null +++ b/playkit/src/test/assets/testdata/mpd/sample_dash_audio_only @@ -0,0 +1,36 @@ + + + + Media Presentation Description for audio only from DASH-IF live simulator + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/playkit/src/test/assets/testdata/mpd/sample_dash_clear_h264_tears b/playkit/src/test/assets/testdata/mpd/sample_dash_clear_h264_tears new file mode 100644 index 000000000..5ae90704d --- /dev/null +++ b/playkit/src/test/assets/testdata/mpd/sample_dash_clear_h264_tears @@ -0,0 +1,41 @@ + + + + + + + + tears_audio_eng.mp4 + + + + + + + + tears_h264_baseline_240p_800.mp4 + + + + + + tears_h264_main_480p_2000.mp4 + + + + + + tears_h264_main_720p_8000.mp4 + + + + + + tears_h264_high_1080p_20000.mp4 + + + + + + + diff --git a/playkit/src/test/assets/testdata/mpd/sample_dash_custom_manifest_image_tracks b/playkit/src/test/assets/testdata/mpd/sample_dash_custom_manifest_image_tracks new file mode 100644 index 000000000..faf79bc00 --- /dev/null +++ b/playkit/src/test/assets/testdata/mpd/sample_dash_custom_manifest_image_tracks @@ -0,0 +1,35 @@ + + ./ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/playkit/src/test/assets/testdata/mpd/sample_dash_custom_manifest_without_image_tracks b/playkit/src/test/assets/testdata/mpd/sample_dash_custom_manifest_without_image_tracks new file mode 100644 index 000000000..78a6e6d94 --- /dev/null +++ b/playkit/src/test/assets/testdata/mpd/sample_dash_custom_manifest_without_image_tracks @@ -0,0 +1,27 @@ + + ./ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/playkit/src/test/assets/testdata/mpd/sample_dash_hevc_and_avc_mix b/playkit/src/test/assets/testdata/mpd/sample_dash_hevc_and_avc_mix new file mode 100644 index 000000000..06148b0e0 --- /dev/null +++ b/playkit/src/test/assets/testdata/mpd/sample_dash_hevc_and_avc_mix @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/playkit/src/test/assets/testdata/mpd/sample_dash_no_tracks b/playkit/src/test/assets/testdata/mpd/sample_dash_no_tracks new file mode 100644 index 000000000..ea8d4a939 --- /dev/null +++ b/playkit/src/test/assets/testdata/mpd/sample_dash_no_tracks @@ -0,0 +1,6 @@ + + + + + + diff --git a/playkit/src/test/assets/testdata/mpd/sample_dash_switching_property b/playkit/src/test/assets/testdata/mpd/sample_dash_switching_property new file mode 100644 index 000000000..4f10ee584 --- /dev/null +++ b/playkit/src/test/assets/testdata/mpd/sample_dash_switching_property @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/playkit/src/test/assets/testdata/mpd/sample_dash_with_external_subtitle b/playkit/src/test/assets/testdata/mpd/sample_dash_with_external_subtitle new file mode 100644 index 000000000..44391bfe1 --- /dev/null +++ b/playkit/src/test/assets/testdata/mpd/sample_dash_with_external_subtitle @@ -0,0 +1,45 @@ + + + + Media Presentation Description by DASH-IF Live Source Simulator. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/playkit/src/test/assets/testdata/mpd/sample_dash_with_subtitle b/playkit/src/test/assets/testdata/mpd/sample_dash_with_subtitle new file mode 100644 index 000000000..7fec4b4fe --- /dev/null +++ b/playkit/src/test/assets/testdata/mpd/sample_dash_with_subtitle @@ -0,0 +1,45 @@ + + + + Media Presentation Description by DASH-IF Live Source Simulator. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/playkit/src/test/java/com/kaltura/playkit/player/ExoPlayerWrapperTest.kt b/playkit/src/test/java/com/kaltura/playkit/player/ExoPlayerWrapperTest.kt new file mode 100644 index 000000000..b6d87f9d7 --- /dev/null +++ b/playkit/src/test/java/com/kaltura/playkit/player/ExoPlayerWrapperTest.kt @@ -0,0 +1,98 @@ +package com.kaltura.playkit.player + +import android.content.Context +import android.os.Looper +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.kaltura.android.exoplayer2.source.TrackGroupArray +import com.kaltura.android.exoplayer2.trackselection.TrackSelectionArray +import com.kaltura.playkit.* +import com.kaltura.testhelper.TestUtils +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Test +import org.junit.function.ThrowingRunnable +import org.junit.runner.RunWith +import org.mockito.Mockito.* +import org.robolectric.Shadows + +@RunWith(AndroidJUnit4::class) +internal class ExoPlayerWrapperTest { + + private var exoPlayerView: BaseExoplayerView? = null + private var rootPlayerView: PlayerView? = null + private var eventListener: PlayerEngine.EventListener? = null + private val playerSettings: PlayerSettings = PlayerSettings() + + private lateinit var realExoPlayerWrapper: ExoPlayerWrapper + private lateinit var mockExoPlayerWrapper: ExoPlayerWrapper + private lateinit var trackSelectionHelper: TrackSelectionHelper + private lateinit var context: Context + private lateinit var emptyTrackSelectionArray: TrackSelectionArray + private lateinit var trackGroupArray: TrackGroupArray + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + exoPlayerView = mock(BaseExoplayerView::class.java) + rootPlayerView = mock(PlayerView::class.java) + eventListener = mock(PlayerEngine.EventListener::class.java) + trackSelectionHelper = mock(TrackSelectionHelper::class.java) + mockExoPlayerWrapper = mock(ExoPlayerWrapper::class.java) + + prepareClassInstances() + + Shadows.shadowOf(Looper.getMainLooper()).idle() + } + + @Test + @Throws(NullPointerException::class) + fun onTrackChanged_TrackSelectionHelper_Exception() { + assertThrows(NullPointerException::class.java, ThrowingRunnable { realExoPlayerWrapper.onTracksChanged(trackGroupArray, emptyTrackSelectionArray) }) + } + + @Test + fun load_flow() { + val pkMediaSourceConfig = getMediaSourceConfig() + mockExoPlayerWrapper.load(pkMediaSourceConfig) + verify(mockExoPlayerWrapper).load(pkMediaSourceConfig) + } + + @Test + fun onTrackChanged_TrackSelectionHelper_Working() { + //FIXME +// realExoPlayerWrapper.load(mock(PKMediaSourceConfig::class.java)) +// val spiedExoPlayerWrapper = spy(realExoPlayerWrapper) +// //exoPlayerWrapper.onTracksChanged(trackGroupArray, emptyTrackSelectionArray) +// `when`(spiedExoPlayerWrapper.onTracksChanged(trackGroupArray, emptyTrackSelectionArray)).then(Answers.RETURNS_SMART_NULLS) + //doNothing().`when`(spiedExoPlayerWrapper).onTracksChanged(trackGroupArray, emptyTrackSelectionArray) + // verify(exoPlayerWrapper, times(1)).onTracksChanged(trackGroupArray, emptyTrackSelectionArray) + } + + private fun prepareClassInstances() { + emptyTrackSelectionArray = TrackSelectionArray() + trackGroupArray = TestUtils.getTrackGroupArrayFromDashManifest(ApplicationProvider.getApplicationContext(), + "testdata/mpd/sample_dash_switching_property") + realExoPlayerWrapper = ExoPlayerWrapper(context, exoPlayerView, playerSettings, rootPlayerView) + realExoPlayerWrapper.setEventListener(eventListener) + } + + private fun getMediaSourceConfig(): PKMediaSourceConfig { + val mediaSourceList: ArrayList = arrayListOf() + val mediaSource = PKMediaSource() + val mediaEntry = PKMediaEntry() + val mediaConfig = PKMediaConfig() + + mediaSource.url = "https://testMedia/playlist.m3u8" + mediaSource.mediaFormat = null + + mediaEntry.duration = 0 + + mediaSourceList.add(mediaSource) + mediaEntry.sources = mediaSourceList + + mediaConfig.mediaEntry = mediaEntry + + return PKMediaSourceConfig(mediaConfig, mediaSource, playerSettings) + } +} diff --git a/playkit/src/test/java/com/kaltura/playkit/player/SourceSelectorTest.kt b/playkit/src/test/java/com/kaltura/playkit/player/SourceSelectorTest.kt new file mode 100644 index 000000000..b171fae9e --- /dev/null +++ b/playkit/src/test/java/com/kaltura/playkit/player/SourceSelectorTest.kt @@ -0,0 +1,61 @@ +package com.kaltura.playkit.player + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.kaltura.playkit.PKDrmParams +import com.kaltura.playkit.PKMediaEntry +import com.kaltura.playkit.PKMediaFormat +import com.kaltura.playkit.PKMediaSource +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SourceSelectorTest { + val MPD_EXAMPLE = "http://example.com/a.mpd" + val MP4_EXAMPLE = "http://example.com/a.mp4" + val M3U8_EXAMPLE = "http://example.com/a.m3u8" + val WVM_EXAMPLE = "http://example.com/a.wvm" + val MP3_EXAMPLE = "http://example.com/a.mp3" + + var dashWidevine = PKMediaSource() + .setMediaFormat(PKMediaFormat.dash) + .setUrl("http://example.com/a.mpd") + .setDrmData(listOf(PKDrmParams("http://whatever", PKDrmParams.Scheme.WidevineCENC))) + + var dashClear = PKMediaSource().setMediaFormat(PKMediaFormat.dash).setUrl(MPD_EXAMPLE) + var mp4 = PKMediaSource().setMediaFormat(PKMediaFormat.mp4).setUrl(MP4_EXAMPLE) + var hls = PKMediaSource().setMediaFormat(PKMediaFormat.hls).setUrl(M3U8_EXAMPLE) + var wvm = PKMediaSource().setMediaFormat(PKMediaFormat.wvm).setUrl(WVM_EXAMPLE) + var mp3 = PKMediaSource().setMediaFormat(PKMediaFormat.mp3).setUrl(MP3_EXAMPLE) + + private fun entry(vararg sources: PKMediaSource): PKMediaEntry? { + return PKMediaEntry().setSources(listOf(*sources)) + } + + @Test + @Throws(Exception::class) + fun mediaFormatByExtension() { + assertEquals(PKMediaFormat.dash, PKMediaFormat.valueOfExt("mpd")) + assertEquals(PKMediaFormat.mp4, PKMediaFormat.valueOfExt("mp4")) + assertEquals(PKMediaFormat.hls, PKMediaFormat.valueOfExt("m3u8")) + assertEquals(PKMediaFormat.wvm, PKMediaFormat.valueOfExt("wvm")) + assertNull(null, PKMediaFormat.valueOfExt("foo")) + assertEquals(PKMediaFormat.udp, PKMediaFormat.valueOfExt(null)) + assertEquals(PKMediaFormat.dash, PKMediaFormat.valueOfUrl(dashClear.url)) + assertEquals(PKMediaFormat.mp4, PKMediaFormat.valueOfUrl(mp4.url)) + assertEquals(PKMediaFormat.hls, PKMediaFormat.valueOfUrl(hls.url)) + assertEquals(PKMediaFormat.wvm, PKMediaFormat.valueOfUrl(wvm.url)) + assertNull(null, PKMediaFormat.valueOfUrl(null)) + } + + @Test + fun sourceSelector() { + assertTrue(SourceSelector.selectSource(entry(mp4, dashClear), PKMediaFormat.dash) === dashClear) + assertTrue(SourceSelector.selectSource(entry(mp4, hls), PKMediaFormat.hls) === hls) + assertTrue(SourceSelector.selectSource(entry(hls, mp4, dashClear), PKMediaFormat.dash) === dashClear) + assertTrue(SourceSelector.selectSource(entry(hls, wvm), PKMediaFormat.dash) === hls) + assertFalse(SourceSelector.selectSource(entry(dashWidevine, wvm, hls, mp4), PKMediaFormat.wvm) === dashWidevine) + assertTrue(SourceSelector.selectSource(entry(mp3), PKMediaFormat.mp3) === mp3) + assertTrue(SourceSelector.selectSource(entry(mp4, wvm), PKMediaFormat.hls) === wvm) + } +} diff --git a/playkit/src/test/java/com/kaltura/playkit/player/TrackSelectionHelperTest.kt b/playkit/src/test/java/com/kaltura/playkit/player/TrackSelectionHelperTest.kt new file mode 100644 index 000000000..a7336d203 --- /dev/null +++ b/playkit/src/test/java/com/kaltura/playkit/player/TrackSelectionHelperTest.kt @@ -0,0 +1,541 @@ +package com.kaltura.playkit.player + +import android.content.Context +import android.net.Uri +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.kaltura.android.exoplayer2.C +import com.kaltura.android.exoplayer2.Format +import com.kaltura.android.exoplayer2.RendererCapabilities +import com.kaltura.android.exoplayer2.dashmanifestparser.CustomDashManifest +import com.kaltura.android.exoplayer2.dashmanifestparser.CustomDashManifestParser +import com.kaltura.android.exoplayer2.dashmanifestparser.CustomFormat +import com.kaltura.android.exoplayer2.source.MediaSource +import com.kaltura.android.exoplayer2.source.TrackGroupArray +import com.kaltura.android.exoplayer2.trackselection.* +import com.kaltura.android.exoplayer2.upstream.BandwidthMeter +import com.kaltura.playkit.* +import com.kaltura.playkit.utils.Consts.TRACK_TYPE_AUDIO +import com.kaltura.testhelper.FakeRendererCapabilities +import com.kaltura.testhelper.FakeTimeline +import com.kaltura.testhelper.TestUtils +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.* + +/** + * TrackSelectionHelper unit test class + * + * We have two types of DefaultTrackSelector (mockedSelector and actualSelector) + * Whenever we need to build actual tracks from manifest, we are using actualSelector + * + * Test manifests are in assets/testdata + * + * We are using Fake classes for testing purpose only. + * Those classes are part of com.kaltura.testhelper + * + * TestUtils class has the methods for manifest parsing and getting the tracks from manifest. + * + * In case if the tracks are present in manifest but not coming in the prepareTracks method's mappedTrackInfo + * then please check the logic in FakeRendererCapabilities + * + * In case if you want to get all the tracks, you need to call prepareTracks of TrackSelectionHelper + */ +@RunWith(AndroidJUnit4::class) +internal class TrackSelectionHelperTest { + + private val initialDashAssetPath: String = "testdata/mpd/" + private val dashFilePrefix: String = "sample_dash" + private val initialHlsAssetPath: String = "testdata/m3u8/" + + private lateinit var trackSelectionArray: TrackSelectionArray + private lateinit var actualSelector: TrackSelector + private var mockedSelector: DefaultTrackSelector = mock(DefaultTrackSelector::class.java) + + private val lastSelectedTrackIds = arrayOf(TrackSelectionHelper.NONE, TrackSelectionHelper.NONE, TrackSelectionHelper.NONE, TrackSelectionHelper.NONE) + + private lateinit var invalidationListener: TrackSelector.InvalidationListener + private lateinit var bandwidthMeter: BandwidthMeter + private lateinit var playerSettings: PlayerSettings + private lateinit var trackGroupArray: TrackGroupArray + + private val VIDEO_CAPABILITIES: RendererCapabilities = FakeRendererCapabilities(C.TRACK_TYPE_VIDEO) + private val AUDIO_CAPABILITIES: RendererCapabilities = FakeRendererCapabilities(C.TRACK_TYPE_AUDIO) + private val TEXT_CAPABILITIES: RendererCapabilities = FakeRendererCapabilities(C.TRACK_TYPE_TEXT) + private val METADATA_CAPABILITIES: RendererCapabilities = FakeRendererCapabilities(C.TRACK_TYPE_METADATA) + private val CAMERA_MOTION_CAPABILITIES: RendererCapabilities = FakeRendererCapabilities(C.TRACK_TYPE_CAMERA_MOTION) + private val RENDERER_CAPABILITIES: Array = + arrayOf(VIDEO_CAPABILITIES, + AUDIO_CAPABILITIES, + TEXT_CAPABILITIES, + METADATA_CAPABILITIES, + CAMERA_MOTION_CAPABILITIES) + + @Before + fun setup() { + invalidationListener = mock(TrackSelector.InvalidationListener::class.java) + bandwidthMeter = mock(BandwidthMeter::class.java) + playerSettings = PlayerSettings() + `when`(bandwidthMeter.bitrateEstimate).thenReturn(100000L) + } + + private fun buildActualTrackSelector(fileName: String): TrackSelector { + val timeline = FakeTimeline() + val filePath: String + + // Checks with type of manifest it is + if (fileName.startsWith(dashFilePrefix)) { + filePath = "${initialDashAssetPath}${fileName}" + trackGroupArray = TestUtils.getTrackGroupArrayFromDashManifest( + ApplicationProvider.getApplicationContext(), + filePath) + } else { + filePath = "${initialHlsAssetPath}${fileName}" + trackGroupArray = TestUtils.getTrackGroupArrayFromHlsManifest( + ApplicationProvider.getApplicationContext(), + filePath) + } + + actualSelector = DefaultTrackSelector(ApplicationProvider.getApplicationContext() as Context) + actualSelector.init(invalidationListener, bandwidthMeter) + + val trackSelectorResult: TrackSelectorResult = actualSelector.selectTracks(RENDERER_CAPABILITIES, + trackGroupArray, + MediaSource.MediaPeriodId(timeline.getUidOfPeriod(0)), + FakeTimeline()) + + val exoTrackSelectionArray = ArrayList() + for (exoTrackSelection: ExoTrackSelection in trackSelectorResult.selections) { + exoTrackSelectionArray.add(exoTrackSelection) + } + + trackSelectionArray = TrackSelectionArray(*exoTrackSelectionArray.toTypedArray()) + + // Mandatory step to get the mappedTrackInfo in TrackSelectionHelper + actualSelector.onSelectionActivated(trackSelectorResult.info) + + return actualSelector + } + + /** + * Build TrackSelectionHelper with real tracks + */ + private fun getTrackSelectionHelper(selector: TrackSelector): TrackSelectionHelper { + val trackSelectionHelper = TrackSelectionHelper( + ApplicationProvider.getApplicationContext(), + selector as DefaultTrackSelector, + lastSelectedTrackIds) + trackSelectionHelper.applyPlayerSettings(playerSettings) + + return trackSelectionHelper + } + + @Test + fun buildTracks_mappedTrackInfo() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_clear_h264_tears")) + trackSelectionHelper.prepareTracks(TrackSelectionArray(), null).let { + assertTrue(it) + } + + val trackSelectionHelperHls = getTrackSelectionHelper(buildActualTrackSelector("sample_hls_harold")) + trackSelectionHelperHls.prepareTracks(TrackSelectionArray(), null).let { + assertTrue(it) + } + } + + @Test + fun buildTracks_mappedTrackInfo_instream_subtitles_dash() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_with_subtitle")) + val spyTrackSelectionHelper = spy(trackSelectionHelper) + spyTrackSelectionHelper.prepareTracks(TrackSelectionArray(), null) + verify(spyTrackSelectionHelper)?.buildTracks(ArrayList()) + + val pkTracks: PKTracks? = spyTrackSelectionHelper.buildTracks(null) + assertEquals(6, pkTracks?.textTracks?.size) + } + + @Test + fun buildTracks_mappedTrackInfo_instream_subtitles_hls() { + val trackSelectionHelperHls = getTrackSelectionHelper(buildActualTrackSelector("sample_hls_harold")) + val spyTrackSelectionHelperHls = spy(trackSelectionHelperHls) + spyTrackSelectionHelperHls?.prepareTracks(TrackSelectionArray(), null) + verify(spyTrackSelectionHelperHls)?.buildTracks(ArrayList()) + + val pkTracksHls: PKTracks? = spyTrackSelectionHelperHls?.buildTracks(null) + assertEquals(8, pkTracksHls?.textTracks?.size) + } + + @Test + fun buildTracks_mappedTrackInfo_is_null() { + val trackSelectionHelper = getTrackSelectionHelper(mockedSelector) + trackSelectionHelper.prepareTracks(TrackSelectionArray(), null).let { + assertFalse(it) + } + } + + @Test + fun buildTracks_mappedTrackInfo_checkTracksUnavailability_false_dash() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_no_tracks")) + trackSelectionHelper.setTracksErrorListener(mock(TrackSelectionHelper.TracksErrorListener::class.java)) + trackSelectionHelper.prepareTracks(TrackSelectionArray(), null).let { + assertFalse(it) + } + } + + @Test + fun buildTracks_rawImageTracks() { + val dashManifestString: String = TestUtils.getManifestString(ApplicationProvider.getApplicationContext() as Context, "${initialDashAssetPath}sample_dash_custom_manifest_image_tracks") + val customDashManifest: CustomDashManifest = CustomDashManifestParser().parse(Uri.parse("http://dash.edgesuite.net/akamai/bbb_30fps/bbb_with_multiple_tiled_thumbnails.mpd"), + dashManifestString) + + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_custom_manifest_without_image_tracks")) + trackSelectionHelper.prepareTracks(TrackSelectionArray(), customDashManifest).let { + assertTrue(it) + } + } + + @Test + fun getThumbnailInfo_imageTracks() { + val dashManifestString: String = TestUtils.getManifestString(ApplicationProvider.getApplicationContext() as Context, "${initialDashAssetPath}sample_dash_custom_manifest_image_tracks") + val customDashManifest: CustomDashManifest = CustomDashManifestParser().parse(Uri.parse("http://dash.edgesuite.net/akamai/bbb_30fps/bbb_with_multiple_tiled_thumbnails.mpd"), + dashManifestString) + + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_custom_manifest_without_image_tracks")) + val spyTrackSelectionHelper = spy(trackSelectionHelper) + spyTrackSelectionHelper.prepareTracks(TrackSelectionArray(), customDashManifest) + val thumbnailInfo = spyTrackSelectionHelper.getThumbnailInfo(160000) + assertEquals("http://dash.edgesuite.net/akamai/bbb_30fps/thumbnails_102x58/tile_1.jpg", thumbnailInfo.url) + } + + @Test + fun getExternalSubtitleLanguage() { + val format = Format.Builder().setLanguage("eng-application/mp4").build() + val trackSelectionHelper = getTrackSelectionHelper(mockedSelector) + assertEquals("en", trackSelectionHelper.getExternalSubtitleLanguage(format)) + + val isExternalSubtitleformat = format.buildUpon().setLanguage("eng-application/mp4").setSampleMimeType("application/mp4").build() + assertEquals(true, trackSelectionHelper.isExternalSubtitle(isExternalSubtitleformat.language, isExternalSubtitleformat.sampleMimeType)) + + val nullFormat = format.buildUpon().setLanguage(null).build() + assertNull(trackSelectionHelper.getExternalSubtitleLanguage(nullFormat)) + } + + @Test(expected = StringIndexOutOfBoundsException::class) + fun getExternalSubtitleLanguage_null_exception() { + val format = Format.Builder().build() + val trackSelectionHelper = getTrackSelectionHelper(mockedSelector) + + val nullExternalSubtitleformat = format.buildUpon().setLanguage("engapplication/mp4").setSampleMimeType("application/mp4").build() + trackSelectionHelper.getExternalSubtitleLanguage(nullExternalSubtitleformat) + } + + @Test + fun discardTextTrackOnPreference() { + val format = Format.Builder().build() + val trackSelectionHelper = getTrackSelectionHelper(mockedSelector) + playerSettings.setSubtitlePreference(PKSubtitlePreference.OFF); + assertFalse(trackSelectionHelper.discardTextTrackOnPreference(format)) + + playerSettings.setSubtitlePreference(PKSubtitlePreference.EXTERNAL) + val japaneseFormat = format.buildUpon().setLanguage("ja-text/vtt").setSampleMimeType("text/vtt").build() + val japaneseInternalFormat = format.buildUpon().setLanguage("ja").setSampleMimeType("text/vtt").build() + val englishFormat = format.buildUpon().setLanguage("en-text/vtt").setSampleMimeType("text/vtt").build() + val formatMapJa: HashMap> = HashMap() + val formatMapEn: HashMap> = HashMap() + formatMapJa.put("ja", listOf(japaneseFormat)) + formatMapJa.put("ja-text/vtt", listOf(japaneseInternalFormat)) + formatMapEn.put("en", listOf(englishFormat)) + trackSelectionHelper.subtitleListMap.put("ja", formatMapJa) + trackSelectionHelper.subtitleListMap.put("en", formatMapEn) + + assertFalse(trackSelectionHelper.discardTextTrackOnPreference(japaneseFormat)) + } + + @Test + fun filterVideoTracks_hevc_dash() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_hevc_and_avc_mix")) + // HEVC only track + val spyTrackSelectionHelper = spy(trackSelectionHelper) + + `when`(spyTrackSelectionHelper.videoCodecsSupportedInHardware()).thenReturn(true) + `when`(spyTrackSelectionHelper.isCodecSupported(anyString(), eq(TrackSelectionHelper.TrackType.VIDEO), anyBoolean())).thenReturn(true) + playerSettings.preferredVideoCodecSettings.allowMixedCodecAdaptiveness = false + spyTrackSelectionHelper.prepareTracks(TrackSelectionArray(), null) + val videoTrack = spyTrackSelectionHelper.filterVideoTracks() + assertEquals(4, videoTrack.size) + assertEquals("HEVC Track Found: ", PKVideoCodec.HEVC, videoTrack.get(0).codecType) + } + + @Test + fun filterVideoTracks_hevc_hls() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_hls_multi_codec")) + // HEVC only track + val spyTrackSelectionHelper = spy(trackSelectionHelper) + + `when`(spyTrackSelectionHelper.videoCodecsSupportedInHardware()).thenReturn(true) + `when`(spyTrackSelectionHelper.isCodecSupported(anyString(), eq(TrackSelectionHelper.TrackType.VIDEO), anyBoolean())).thenReturn(true) + playerSettings.preferredVideoCodecSettings.allowMixedCodecAdaptiveness = false + spyTrackSelectionHelper.prepareTracks(TrackSelectionArray(), null) + val videoTrack = spyTrackSelectionHelper.filterVideoTracks() + assertEquals(2, videoTrack.size) + assertEquals("HEVC Track Found: ", PKVideoCodec.HEVC, videoTrack.get(0).codecType) + } + + @Test + fun filterVideoTracks_avc_dash() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_hevc_and_avc_mix")) + // AVC only track + val spyTrackSelectionHelper = spy(trackSelectionHelper) + + `when`(spyTrackSelectionHelper.videoCodecsSupportedInHardware()).thenReturn(true) + `when`(spyTrackSelectionHelper.isCodecSupported(anyString(), eq(TrackSelectionHelper.TrackType.VIDEO), anyBoolean())).thenReturn(true) + playerSettings.preferredVideoCodecSettings = VideoCodecSettings().setCodecPriorityList(listOf(PKVideoCodec.AVC, PKVideoCodec.HEVC)) + spyTrackSelectionHelper.prepareTracks(TrackSelectionArray(), null) + val videoTrack = spyTrackSelectionHelper.filterVideoTracks() + assertEquals(5, videoTrack.size) + assertEquals("AVC Track Found: ", PKVideoCodec.AVC, videoTrack.get(0).codecType) + } + + @Test + fun filterVideoTracks_avc_hls() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_hls_multi_codec")) + // AVC only track + val spyTrackSelectionHelper = spy(trackSelectionHelper) + + `when`(spyTrackSelectionHelper.videoCodecsSupportedInHardware()).thenReturn(true) + `when`(spyTrackSelectionHelper.isCodecSupported(anyString(), eq(TrackSelectionHelper.TrackType.VIDEO), anyBoolean())).thenReturn(true) + playerSettings.preferredVideoCodecSettings = VideoCodecSettings().setCodecPriorityList(listOf(PKVideoCodec.AVC, PKVideoCodec.HEVC)) + spyTrackSelectionHelper.prepareTracks(TrackSelectionArray(), null) + val videoTrack = spyTrackSelectionHelper.filterVideoTracks() + assertEquals(3, videoTrack.size) + assertEquals("AVC Track Found: ", PKVideoCodec.AVC, videoTrack.get(0).codecType) + } + + @Test + fun filterAdaptiveAudioTracks_ec3() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_audio_only")) + val spyTrackSelectionHelper = spy(trackSelectionHelper) + + spyTrackSelectionHelper.prepareTracks(TrackSelectionArray(), null) + playerSettings.preferredAudioCodecSettings = AudioCodecSettings().setCodecPriorityList(listOf(PKAudioCodec.E_AC3, PKAudioCodec.AAC)) + val audioTrack = spyTrackSelectionHelper.filterAdaptiveAudioTracks() + assertEquals(2, audioTrack.size) + } + + @Test + fun getDefaultTrackIndex() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_with_external_subtitle")) + val spyTrackSelectionHelper = spy(trackSelectionHelper) + + spyTrackSelectionHelper.prepareTracks(trackSelectionArray, null) + spyTrackSelectionHelper.hasExternalSubtitlesInTracks = true + playerSettings.subtitlePreference = PKSubtitlePreference.EXTERNAL + assertEquals(2, spyTrackSelectionHelper.getDefaultTrackIndex(spyTrackSelectionHelper.textTracks, TrackSelectionHelper.NONE)) + } + + @Test + fun extractTextTracksToMap() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_with_external_subtitle")) + val spyTrackSelectionHelper = spy(trackSelectionHelper) + + spyTrackSelectionHelper.hasExternalSubtitles = true + spyTrackSelectionHelper.prepareTracks(trackSelectionArray, null) + assertEquals(4, spyTrackSelectionHelper.subtitleListMap.size) + assertEquals(2, spyTrackSelectionHelper.subtitleListMap["en"]?.size) + } + + @Test + fun getVideoTracksUniqueIds() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_with_external_subtitle")) + val spyTrackSelectionHelper = spy(trackSelectionHelper) + + spyTrackSelectionHelper.prepareTracks(trackSelectionArray, null) + assertEquals(1, spyTrackSelectionHelper.videoTracks.size) + } + + @Test + fun getCodecUniqueIdsWithABR_dash() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_clear_h264_tears")) + val spyTrackSelectionHelper = spy(trackSelectionHelper) + spyTrackSelectionHelper.setTracksErrorListener(mock(TrackSelectionHelper.TracksErrorListener::class.java)) + spyTrackSelectionHelper.prepareTracks(trackSelectionArray, null) + + assertEquals(4, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(769255, 7203938, PKAbrFilter.BITRATE).size) + assertEquals(2, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(0, 769255, PKAbrFilter.BITRATE).size) + assertEquals(5, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(0, 769254, PKAbrFilter.BITRATE).size) + assertEquals(2, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(769256, 1774250, PKAbrFilter.BITRATE).size) + assertEquals(4, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(1774254, 18316946, PKAbrFilter.BITRATE).size) + assertNotEquals(4, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(18316946, 183169469, PKAbrFilter.BITRATE).size) + assertEquals(5, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(-1, 2000000000, PKAbrFilter.BITRATE).size) + + // DO NOT TEST THIS CASE BECAUSE maxAbr > minAbr check is on ExoPlayerWrapper level. This cases will be invalid on TrackSelectionHelper level. + //assertEquals(5, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(1774250, 769256, PKAbrFilter.BITRATE).size) + + assertEquals(3, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(142, 380, PKAbrFilter.HEIGHT).size) + assertEquals(3, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(380, 570, PKAbrFilter.HEIGHT).size) + assertEquals(3, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(570, 856, PKAbrFilter.HEIGHT).size) + assertEquals(2, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(150, 350, PKAbrFilter.HEIGHT).size) + assertEquals(5, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(0, 140, PKAbrFilter.HEIGHT).size) + assertEquals(5, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(900, 10000, PKAbrFilter.HEIGHT).size) + + assertEquals(3, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(320, 854, PKAbrFilter.WIDTH).size) + assertEquals(3, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(854, 1280, PKAbrFilter.WIDTH).size) + assertEquals(3, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(1280, 1920, PKAbrFilter.WIDTH).size) + assertEquals(2, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(310, 350, PKAbrFilter.WIDTH).size) + assertEquals(5, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(0, 310, PKAbrFilter.WIDTH).size) + assertEquals(5, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(2000, 10000, PKAbrFilter.WIDTH).size) + + assertEquals(4, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(45440, 729600, PKAbrFilter.PIXEL).size) + } + + @Test + fun getCodecUniqueIdsWithABR_hls() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_hls_abr_test")) + val spyTrackSelectionHelper = spy(trackSelectionHelper) + spyTrackSelectionHelper.setTracksErrorListener(mock(TrackSelectionHelper.TracksErrorListener::class.java)) + spyTrackSelectionHelper.prepareTracks(trackSelectionArray, null) + + assertEquals(4, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(769255, 7203938, PKAbrFilter.BITRATE).size) + assertEquals(2, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(0, 769255, PKAbrFilter.BITRATE).size) + assertEquals(5, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(0, 769254, PKAbrFilter.BITRATE).size) + assertEquals(2, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(769256, 1774250, PKAbrFilter.BITRATE).size) + assertEquals(4, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(1774254, 18316946, PKAbrFilter.BITRATE).size) + assertNotEquals(4, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(18316946, 183169469, PKAbrFilter.BITRATE).size) + assertEquals(5, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(-1, 2000000000, PKAbrFilter.BITRATE).size) + + // Don't test this because maxAbr > minAbr check is on ExoPlayerWrapper level. This cases will be invalid on TrackSelectionHelper level. + //assertEquals(5, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(1774250, 769256, PKAbrFilter.BITRATE).size) + + assertEquals(3, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(142, 380, PKAbrFilter.HEIGHT).size) + assertEquals(3, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(380, 570, PKAbrFilter.HEIGHT).size) + assertEquals(3, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(570, 856, PKAbrFilter.HEIGHT).size) + assertEquals(2, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(150, 350, PKAbrFilter.HEIGHT).size) + assertEquals(5, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(0, 140, PKAbrFilter.HEIGHT).size) + assertEquals(5, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(900, 10000, PKAbrFilter.HEIGHT).size) + + assertEquals(3, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(320, 854, PKAbrFilter.WIDTH).size) + assertEquals(3, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(854, 1280, PKAbrFilter.WIDTH).size) + assertEquals(3, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(1280, 1920, PKAbrFilter.WIDTH).size) + assertEquals(2, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(310, 350, PKAbrFilter.WIDTH).size) + assertEquals(5, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(0, 310, PKAbrFilter.WIDTH).size) + assertEquals(5, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(2000, 10000, PKAbrFilter.WIDTH).size) + + assertEquals(4, spyTrackSelectionHelper.getCodecUniqueIdsWithABR(45440, 729600, PKAbrFilter.PIXEL).size) + } + + @Test + fun changeTrack_video() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_clear_h264_tears")) + val spyTrackSelectionHelper = spy(trackSelectionHelper) + spyTrackSelectionHelper.setTracksErrorListener(mock(TrackSelectionHelper.TracksErrorListener::class.java)) + spyTrackSelectionHelper.prepareTracks(trackSelectionArray, null) + + spyTrackSelectionHelper.changeTrack("Video:0,0,1") + verify(spyTrackSelectionHelper).overrideTrack(eq(0), eq(DefaultTrackSelector.SelectionOverride(0, 1)), any()) + } + + @Test + fun changeTrack_video_adaptive() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_clear_h264_tears")) + val spyTrackSelectionHelper = spy(trackSelectionHelper) + spyTrackSelectionHelper.setTracksErrorListener(mock(TrackSelectionHelper.TracksErrorListener::class.java)) + spyTrackSelectionHelper.prepareTracks(trackSelectionArray, null) + + spyTrackSelectionHelper.changeTrack("Video:0,0,-1") + verify(spyTrackSelectionHelper).overrideTrack(eq(0), eq(DefaultTrackSelector.SelectionOverride(0, 0,1,2,3)), any()) + } + + @Test + fun changeTrack_subtitle() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_with_subtitle")) + val spyTrackSelectionHelper = spy(trackSelectionHelper) + spyTrackSelectionHelper.setTracksErrorListener(mock(TrackSelectionHelper.TracksErrorListener::class.java)) + spyTrackSelectionHelper.prepareTracks(trackSelectionArray, null) + + spyTrackSelectionHelper.changeTrack("Text:2,1,0") + verify(spyTrackSelectionHelper).overrideTrack(eq(2), eq(DefaultTrackSelector.SelectionOverride(1, 0)), any()) + } + + @Test + fun changeTrack_audio() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_audio_only")) + val spyTrackSelectionHelper = spy(trackSelectionHelper) + spyTrackSelectionHelper.setTracksErrorListener(mock(TrackSelectionHelper.TracksErrorListener::class.java)) + spyTrackSelectionHelper.prepareTracks(trackSelectionArray, null) + + spyTrackSelectionHelper.changeTrack("Audio:1,1,0") + verify(spyTrackSelectionHelper).overrideTrack(eq(1), eq(DefaultTrackSelector.SelectionOverride(1, 0)), any()) + } + + @Test + fun overrideMediaVideoCodec() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_clear_h264_tears")) + val spyTrackSelectionHelper = spy(trackSelectionHelper) + spyTrackSelectionHelper.setTracksErrorListener(mock(TrackSelectionHelper.TracksErrorListener::class.java)) + spyTrackSelectionHelper.prepareTracks(trackSelectionArray, null) + + spyTrackSelectionHelper.overrideMediaVideoCodec() + verify(spyTrackSelectionHelper).overrideTrack(eq(0), eq(DefaultTrackSelector.SelectionOverride(0, 0,1,2,3)), any()) + } + + @Test + fun overrideMediaDefaultABR() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_clear_h264_tears")) + val spyTrackSelectionHelper = spy(trackSelectionHelper) + spyTrackSelectionHelper.setTracksErrorListener(mock(TrackSelectionHelper.TracksErrorListener::class.java)) + spyTrackSelectionHelper.prepareTracks(trackSelectionArray, null) + + spyTrackSelectionHelper.overrideMediaDefaultABR(0, 1774254, PKAbrFilter.BITRATE) + verify(spyTrackSelectionHelper).overrideTrack(eq(0), eq(DefaultTrackSelector.SelectionOverride(0, 0,1)), any()) + } + + @Test + fun overrideAutoABRTracks_video() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_clear_h264_tears")) + val spyTrackSelectionHelper = spy(trackSelectionHelper) + spyTrackSelectionHelper.setTracksErrorListener(mock(TrackSelectionHelper.TracksErrorListener::class.java)) + spyTrackSelectionHelper.prepareTracks(trackSelectionArray, null) + + val selectionOverride = spyTrackSelectionHelper.overrideAutoABRTracks(0, 0) + assertEquals(4, selectionOverride.tracks.size) + } + + @Test + fun overrideAutoABRTracks_audio() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_audio_only")) + val spyTrackSelectionHelper = spy(trackSelectionHelper) + spyTrackSelectionHelper.setTracksErrorListener(mock(TrackSelectionHelper.TracksErrorListener::class.java)) + spyTrackSelectionHelper.prepareTracks(trackSelectionArray, null) + + val selectionOverride = spyTrackSelectionHelper.overrideAutoABRTracks(1, 0) + assertEquals(1, selectionOverride.tracks.size) + } + + @Test + fun getCurrentVideoBitrate() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_clear_h264_tears")) + val spyTrackSelectionHelper = spy(trackSelectionHelper) + spyTrackSelectionHelper.setTracksErrorListener(mock(TrackSelectionHelper.TracksErrorListener::class.java)) + spyTrackSelectionHelper.prepareTracks(trackSelectionArray, null) + + spyTrackSelectionHelper.changeTrack("Video:0,0,1") + + assertEquals(1774254, spyTrackSelectionHelper.currentVideoBitrate) + assertEquals(131596, spyTrackSelectionHelper.currentAudioBitrate) + assertEquals(380, spyTrackSelectionHelper.currentVideoHeight) + assertEquals(854, spyTrackSelectionHelper.currentVideoWidth) + } + + @Test + fun getPreferredTrackId() { + val trackSelectionHelper = getTrackSelectionHelper(buildActualTrackSelector("sample_dash_clear_h264_tears")) + val spyTrackSelectionHelper = spy(trackSelectionHelper) + spyTrackSelectionHelper.setTracksErrorListener(mock(TrackSelectionHelper.TracksErrorListener::class.java)) + spyTrackSelectionHelper.prepareTracks(trackSelectionArray, null) + playerSettings.setPreferredAudioTrack(PKTrackConfig().setPreferredMode(PKTrackConfig.Mode.AUTO)) + + assertEquals("Audio:1,0,0", spyTrackSelectionHelper.getPreferredTrackId(TRACK_TYPE_AUDIO)) + } +} diff --git a/playkit/src/test/resources/robolectric.properties b/playkit/src/test/resources/robolectric.properties new file mode 100644 index 000000000..932b01b9e --- /dev/null +++ b/playkit/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +sdk=28