diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e8779c4ccc..41836c2648 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -24,6 +24,8 @@ * Cronet Extension: * RTMP Extension: * HLS Extension: +* DASH Extension: + * Allow multiple of the same DASH identifier in segment template url. * Smooth Streaming Extension: * RTSP Extension: * Decoder Extensions (FFmpeg, VP9, AV1, etc.): diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/UrlTemplate.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/UrlTemplate.java index 8d36fce52c..083a1b34b3 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/UrlTemplate.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/UrlTemplate.java @@ -16,6 +16,8 @@ package androidx.media3.exoplayer.dash.manifest; import androidx.media3.common.util.UnstableApi; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; /** @@ -38,10 +40,9 @@ public final class UrlTemplate { private static final int BANDWIDTH_ID = 3; private static final int TIME_ID = 4; - private final String[] urlPieces; - private final int[] identifiers; - private final String[] identifierFormatTags; - private final int identifierCount; + private final List urlPieces; + private final List identifiers; + private final List identifierFormatTags; /** * Compile an instance from the provided template string. @@ -51,22 +52,20 @@ public final class UrlTemplate { * @throws IllegalArgumentException If the template string is malformed. */ public static UrlTemplate compile(String template) { - // These arrays are sizes assuming each of the four possible identifiers will be present at - // most once in the template, which seems like a reasonable assumption. - String[] urlPieces = new String[5]; - int[] identifiers = new int[4]; - String[] identifierFormatTags = new String[4]; - int identifierCount = parseTemplate(template, urlPieces, identifiers, identifierFormatTags); - return new UrlTemplate(urlPieces, identifiers, identifierFormatTags, identifierCount); + List urlPieces = new ArrayList<>(); + List identifiers = new ArrayList<>(); + List identifierFormatTags = new ArrayList<>(); + + parseTemplate(template, urlPieces, identifiers, identifierFormatTags); + return new UrlTemplate(urlPieces, identifiers, identifierFormatTags); } /** Internal constructor. Use {@link #compile(String)} to build instances of this class. */ private UrlTemplate( - String[] urlPieces, int[] identifiers, String[] identifierFormatTags, int identifierCount) { + List urlPieces, List identifiers, List identifierFormatTags) { this.urlPieces = urlPieces; this.identifiers = identifiers; this.identifierFormatTags = identifierFormatTags; - this.identifierCount = identifierCount; } /** @@ -82,58 +81,65 @@ public final class UrlTemplate { */ public String buildUri(String representationId, long segmentNumber, int bandwidth, long time) { StringBuilder builder = new StringBuilder(); - for (int i = 0; i < identifierCount; i++) { - builder.append(urlPieces[i]); - if (identifiers[i] == REPRESENTATION_ID) { + for (int i = 0; i < identifiers.size(); i++) { + builder.append(urlPieces.get(i)); + if (identifiers.get(i) == REPRESENTATION_ID) { builder.append(representationId); - } else if (identifiers[i] == NUMBER_ID) { - builder.append(String.format(Locale.US, identifierFormatTags[i], segmentNumber)); - } else if (identifiers[i] == BANDWIDTH_ID) { - builder.append(String.format(Locale.US, identifierFormatTags[i], bandwidth)); - } else if (identifiers[i] == TIME_ID) { - builder.append(String.format(Locale.US, identifierFormatTags[i], time)); + } else if (identifiers.get(i) == NUMBER_ID) { + builder.append(String.format(Locale.US, identifierFormatTags.get(i), segmentNumber)); + } else if (identifiers.get(i) == BANDWIDTH_ID) { + builder.append(String.format(Locale.US, identifierFormatTags.get(i), bandwidth)); + } else if (identifiers.get(i) == TIME_ID) { + builder.append(String.format(Locale.US, identifierFormatTags.get(i), time)); } } - builder.append(urlPieces[identifierCount]); + builder.append(urlPieces.get(identifiers.size())); return builder.toString(); } /** - * Parses {@code template}, placing the decomposed components into the provided arrays. + * Parses {@code template}, placing the decomposed components into the provided lists. * - *

If the return value is N, {@code urlPieces} will contain (N+1) strings that must be - * interleaved with N arguments in order to construct a url. The N identifiers that correspond to - * the required arguments, together with the tags that define their required formatting, are - * returned in {@code identifiers} and {@code identifierFormatTags} respectively. + *

If the number of identifiers in the {@code template} is N, {@code urlPieces} will contain + * (N+1) strings that must be interleaved with those N arguments in order to construct a url. The + * N identifiers that correspond to the required arguments, together with the tags that define + * their required formatting, are returned in {@code identifiers} and {@code identifierFormatTags} + * respectively. * * @param template The template to parse. * @param urlPieces A holder for pieces of url parsed from the template. * @param identifiers A holder for identifiers parsed from the template. * @param identifierFormatTags A holder for format tags corresponding to the parsed identifiers. - * @return The number of identifiers in the template url. * @throws IllegalArgumentException If the template string is malformed. */ - private static int parseTemplate( - String template, String[] urlPieces, int[] identifiers, String[] identifierFormatTags) { - urlPieces[0] = ""; + private static void parseTemplate( + String template, + List urlPieces, + List identifiers, + List identifierFormatTags) { + urlPieces.add(""); int templateIndex = 0; - int identifierCount = 0; while (templateIndex < template.length()) { int dollarIndex = template.indexOf("$", templateIndex); if (dollarIndex == -1) { - urlPieces[identifierCount] += template.substring(templateIndex); + urlPieces.set( + identifiers.size(), + urlPieces.get(identifiers.size()) + template.substring(templateIndex)); templateIndex = template.length(); } else if (dollarIndex != templateIndex) { - urlPieces[identifierCount] += template.substring(templateIndex, dollarIndex); + urlPieces.set( + identifiers.size(), + urlPieces.get(identifiers.size()) + template.substring(templateIndex, dollarIndex)); templateIndex = dollarIndex; } else if (template.startsWith(ESCAPED_DOLLAR, templateIndex)) { - urlPieces[identifierCount] += "$"; + urlPieces.set(identifiers.size(), urlPieces.get(identifiers.size()) + "$"); templateIndex += 2; } else { + identifierFormatTags.add(""); int secondIndex = template.indexOf("$", templateIndex + 1); String identifier = template.substring(templateIndex + 1, secondIndex); if (identifier.equals(REPRESENTATION)) { - identifiers[identifierCount] = REPRESENTATION_ID; + identifiers.add(REPRESENTATION_ID); } else { int formatTagIndex = identifier.indexOf("%0"); String formatTag = DEFAULT_FORMAT_TAG; @@ -149,24 +155,22 @@ public final class UrlTemplate { } switch (identifier) { case NUMBER: - identifiers[identifierCount] = NUMBER_ID; + identifiers.add(NUMBER_ID); break; case BANDWIDTH: - identifiers[identifierCount] = BANDWIDTH_ID; + identifiers.add(BANDWIDTH_ID); break; case TIME: - identifiers[identifierCount] = TIME_ID; + identifiers.add(TIME_ID); break; default: throw new IllegalArgumentException("Invalid template: " + template); } - identifierFormatTags[identifierCount] = formatTag; + identifierFormatTags.set(identifiers.size() - 1, formatTag); } - identifierCount++; - urlPieces[identifierCount] = ""; + urlPieces.add(""); templateIndex = secondIndex + 1; } } - return identifierCount; } } diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/UrlTemplateTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/UrlTemplateTest.java index 362a2c8f82..9cdf9f3bb2 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/UrlTemplateTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/UrlTemplateTest.java @@ -70,4 +70,22 @@ public class UrlTemplateTest { // Expected. } } + + @Test + public void fullWithMultipleOccurrences() { + String template = + "$Bandwidth$_a1_$RepresentationID$_b1_$Time$_c1_$Number$_$Bandwidth$_a2_$RepresentationID$_b2_$Time$_c2_$Number$"; + UrlTemplate urlTemplate = UrlTemplate.compile(template); + String url = urlTemplate.buildUri("abc1", 10, 650000, 5000); + assertThat(url).isEqualTo("650000_a1_abc1_b1_5000_c1_10_650000_a2_abc1_b2_5000_c2_10"); + } + + @Test + public void fullWithMultipleOccurrencesAndDollarEscaping() { + String template = + "$$$Bandwidth$$$_a1$$_$RepresentationID$_b1_$Time$_c1_$Number$$$_$$$Bandwidth$$$_a2$$_$RepresentationID$_b2_$Time$_c2_$Number$$$"; + UrlTemplate urlTemplate = UrlTemplate.compile(template); + String url = urlTemplate.buildUri("abc1", 10, 650000, 5000); + assertThat(url).isEqualTo("$650000$_a1$_abc1_b1_5000_c1_10$_$650000$_a2$_abc1_b2_5000_c2_10$"); + } }