1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.exoplayer2.source.dash.manifest; 17 18 import java.util.Locale; 19 20 /** 21 * A template from which URLs can be built. 22 * <p> 23 * URLs are built according to the substitution rules defined in ISO/IEC 23009-1:2014 5.3.9.4.4. 24 */ 25 public final class UrlTemplate { 26 27 private static final String REPRESENTATION = "RepresentationID"; 28 private static final String NUMBER = "Number"; 29 private static final String BANDWIDTH = "Bandwidth"; 30 private static final String TIME = "Time"; 31 private static final String ESCAPED_DOLLAR = "$$"; 32 private static final String DEFAULT_FORMAT_TAG = "%01d"; 33 34 private static final int REPRESENTATION_ID = 1; 35 private static final int NUMBER_ID = 2; 36 private static final int BANDWIDTH_ID = 3; 37 private static final int TIME_ID = 4; 38 39 private final String[] urlPieces; 40 private final int[] identifiers; 41 private final String[] identifierFormatTags; 42 private final int identifierCount; 43 44 /** 45 * Compile an instance from the provided template string. 46 * 47 * @param template The template. 48 * @return The compiled instance. 49 * @throws IllegalArgumentException If the template string is malformed. 50 */ compile(String template)51 public static UrlTemplate compile(String template) { 52 // These arrays are sizes assuming each of the four possible identifiers will be present at 53 // most once in the template, which seems like a reasonable assumption. 54 String[] urlPieces = new String[5]; 55 int[] identifiers = new int[4]; 56 String[] identifierFormatTags = new String[4]; 57 int identifierCount = parseTemplate(template, urlPieces, identifiers, identifierFormatTags); 58 return new UrlTemplate(urlPieces, identifiers, identifierFormatTags, identifierCount); 59 } 60 61 /** 62 * Internal constructor. Use {@link #compile(String)} to build instances of this class. 63 */ UrlTemplate(String[] urlPieces, int[] identifiers, String[] identifierFormatTags, int identifierCount)64 private UrlTemplate(String[] urlPieces, int[] identifiers, String[] identifierFormatTags, 65 int identifierCount) { 66 this.urlPieces = urlPieces; 67 this.identifiers = identifiers; 68 this.identifierFormatTags = identifierFormatTags; 69 this.identifierCount = identifierCount; 70 } 71 72 /** 73 * Constructs a Uri from the template, substituting in the provided arguments. 74 * 75 * <p>Arguments whose corresponding identifiers are not present in the template will be ignored. 76 * 77 * @param representationId The representation identifier. 78 * @param segmentNumber The segment number. 79 * @param bandwidth The bandwidth. 80 * @param time The time as specified by the segment timeline. 81 * @return The built Uri. 82 */ buildUri(String representationId, long segmentNumber, int bandwidth, long time)83 public String buildUri(String representationId, long segmentNumber, int bandwidth, long time) { 84 StringBuilder builder = new StringBuilder(); 85 for (int i = 0; i < identifierCount; i++) { 86 builder.append(urlPieces[i]); 87 if (identifiers[i] == REPRESENTATION_ID) { 88 builder.append(representationId); 89 } else if (identifiers[i] == NUMBER_ID) { 90 builder.append(String.format(Locale.US, identifierFormatTags[i], segmentNumber)); 91 } else if (identifiers[i] == BANDWIDTH_ID) { 92 builder.append(String.format(Locale.US, identifierFormatTags[i], bandwidth)); 93 } else if (identifiers[i] == TIME_ID) { 94 builder.append(String.format(Locale.US, identifierFormatTags[i], time)); 95 } 96 } 97 builder.append(urlPieces[identifierCount]); 98 return builder.toString(); 99 } 100 101 /** 102 * Parses {@code template}, placing the decomposed components into the provided arrays. 103 * <p> 104 * If the return value is N, {@code urlPieces} will contain (N+1) strings that must be 105 * interleaved with N arguments in order to construct a url. The N identifiers that correspond to 106 * the required arguments, together with the tags that define their required formatting, are 107 * returned in {@code identifiers} and {@code identifierFormatTags} respectively. 108 * 109 * @param template The template to parse. 110 * @param urlPieces A holder for pieces of url parsed from the template. 111 * @param identifiers A holder for identifiers parsed from the template. 112 * @param identifierFormatTags A holder for format tags corresponding to the parsed identifiers. 113 * @return The number of identifiers in the template url. 114 * @throws IllegalArgumentException If the template string is malformed. 115 */ parseTemplate(String template, String[] urlPieces, int[] identifiers, String[] identifierFormatTags)116 private static int parseTemplate(String template, String[] urlPieces, int[] identifiers, 117 String[] identifierFormatTags) { 118 urlPieces[0] = ""; 119 int templateIndex = 0; 120 int identifierCount = 0; 121 while (templateIndex < template.length()) { 122 int dollarIndex = template.indexOf("$", templateIndex); 123 if (dollarIndex == -1) { 124 urlPieces[identifierCount] += template.substring(templateIndex); 125 templateIndex = template.length(); 126 } else if (dollarIndex != templateIndex) { 127 urlPieces[identifierCount] += template.substring(templateIndex, dollarIndex); 128 templateIndex = dollarIndex; 129 } else if (template.startsWith(ESCAPED_DOLLAR, templateIndex)) { 130 urlPieces[identifierCount] += "$"; 131 templateIndex += 2; 132 } else { 133 int secondIndex = template.indexOf("$", templateIndex + 1); 134 String identifier = template.substring(templateIndex + 1, secondIndex); 135 if (identifier.equals(REPRESENTATION)) { 136 identifiers[identifierCount] = REPRESENTATION_ID; 137 } else { 138 int formatTagIndex = identifier.indexOf("%0"); 139 String formatTag = DEFAULT_FORMAT_TAG; 140 if (formatTagIndex != -1) { 141 formatTag = identifier.substring(formatTagIndex); 142 // Allowed conversions are decimal integer (which is the only conversion allowed by the 143 // DASH specification) and hexadecimal integer (due to existing content that uses it). 144 // Else we assume that the conversion is missing, and that it should be decimal integer. 145 if (!formatTag.endsWith("d") && !formatTag.endsWith("x")) { 146 formatTag += "d"; 147 } 148 identifier = identifier.substring(0, formatTagIndex); 149 } 150 switch (identifier) { 151 case NUMBER: 152 identifiers[identifierCount] = NUMBER_ID; 153 break; 154 case BANDWIDTH: 155 identifiers[identifierCount] = BANDWIDTH_ID; 156 break; 157 case TIME: 158 identifiers[identifierCount] = TIME_ID; 159 break; 160 default: 161 throw new IllegalArgumentException("Invalid template: " + template); 162 } 163 identifierFormatTags[identifierCount] = formatTag; 164 } 165 identifierCount++; 166 urlPieces[identifierCount] = ""; 167 templateIndex = secondIndex + 1; 168 } 169 } 170 return identifierCount; 171 } 172 173 } 174