1/* 2 * Copyright (C) 2011, 2012 Apple Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions 6 * are met: 7 * 1. Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * 2. Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * 13 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY 14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR 17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 */ 25 26#include "config.h" 27 28#if ENABLE(VIDEO) 29 30#include "MediaFragmentURIParser.h" 31 32#include "HTMLElement.h" 33#include "MediaPlayer.h" 34#include "ProcessingInstruction.h" 35#include "SegmentedString.h" 36#include "Text.h" 37#include <wtf/text/CString.h> 38#include <wtf/text/StringBuilder.h> 39#include <wtf/text/WTFString.h> 40 41namespace WebCore { 42 43const int secondsPerHour = 3600; 44const int secondsPerMinute = 60; 45const unsigned nptIdentiferLength = 4; // "npt:" 46 47static String collectDigits(const LChar* input, unsigned length, unsigned& position) 48{ 49 StringBuilder digits; 50 51 // http://www.ietf.org/rfc/rfc2326.txt 52 // DIGIT ; any positive number 53 while (position < length && isASCIIDigit(input[position])) 54 digits.append(input[position++]); 55 return digits.toString(); 56} 57 58static String collectFraction(const LChar* input, unsigned length, unsigned& position) 59{ 60 StringBuilder digits; 61 62 // http://www.ietf.org/rfc/rfc2326.txt 63 // [ "." *DIGIT ] 64 if (input[position] != '.') 65 return String(); 66 67 digits.append(input[position++]); 68 while (position < length && isASCIIDigit(input[position])) 69 digits.append(input[position++]); 70 return digits.toString(); 71} 72 73double MediaFragmentURIParser::invalidTimeValue() 74{ 75 return MediaPlayer::invalidTime(); 76} 77 78MediaFragmentURIParser::MediaFragmentURIParser(const KURL& url) 79 : m_url(url) 80 , m_timeFormat(None) 81 , m_startTime(MediaPlayer::invalidTime()) 82 , m_endTime(MediaPlayer::invalidTime()) 83{ 84} 85 86double MediaFragmentURIParser::startTime() 87{ 88 if (!m_url.isValid()) 89 return MediaPlayer::invalidTime(); 90 if (m_timeFormat == None) 91 parseTimeFragment(); 92 return m_startTime; 93} 94 95double MediaFragmentURIParser::endTime() 96{ 97 if (!m_url.isValid()) 98 return MediaPlayer::invalidTime(); 99 if (m_timeFormat == None) 100 parseTimeFragment(); 101 return m_endTime; 102} 103 104void MediaFragmentURIParser::parseFragments() 105{ 106 if (!m_url.hasFragmentIdentifier()) 107 return; 108 String fragmentString = m_url.fragmentIdentifier(); 109 if (fragmentString.isEmpty()) 110 return; 111 112 unsigned offset = 0; 113 unsigned end = fragmentString.length(); 114 while (offset < end) { 115 // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#processing-name-value-components 116 // 1. Parse the octet string according to the namevalues syntax, yielding a list of 117 // name-value pairs, where name and value are both octet string. In accordance 118 // with RFC 3986, the name and value components must be parsed and separated before 119 // percent-encoded octets are decoded. 120 size_t parameterStart = offset; 121 size_t parameterEnd = fragmentString.find('&', offset); 122 if (parameterEnd == notFound) 123 parameterEnd = end; 124 125 size_t equalOffset = fragmentString.find('=', offset); 126 if (equalOffset == notFound || equalOffset > parameterEnd) { 127 offset = parameterEnd + 1; 128 continue; 129 } 130 131 // 2. For each name-value pair: 132 // a. Decode percent-encoded octets in name and value as defined by RFC 3986. If either 133 // name or value are not valid percent-encoded strings, then remove the name-value pair 134 // from the list. 135 const UChar* fragmentStart = fragmentString.characters(); 136 String name = decodeURLEscapeSequences(String(fragmentStart + parameterStart, equalOffset - parameterStart)); 137 String value; 138 if (equalOffset != parameterEnd) 139 value = decodeURLEscapeSequences(String(fragmentStart + equalOffset + 1, parameterEnd - equalOffset - 1)); 140 141 // b. Convert name and value to Unicode strings by interpreting them as UTF-8. If either 142 // name or value are not valid UTF-8 strings, then remove the name-value pair from the list. 143 bool validUTF8 = true; 144 if (!name.isEmpty()) { 145 name = name.utf8(String::StrictConversion).data(); 146 validUTF8 = !name.isEmpty(); 147 } 148 if (validUTF8 && !value.isEmpty()) { 149 value = value.utf8(String::StrictConversion).data(); 150 validUTF8 = !value.isEmpty(); 151 } 152 153 if (validUTF8) 154 m_fragments.append(std::make_pair(name, value)); 155 156 offset = parameterEnd + 1; 157 } 158} 159 160void MediaFragmentURIParser::parseTimeFragment() 161{ 162 ASSERT(m_timeFormat == None); 163 164 if (m_fragments.isEmpty()) 165 parseFragments(); 166 167 m_timeFormat = Invalid; 168 169 for (unsigned i = 0; i < m_fragments.size(); ++i) { 170 pair<String, String>& fragment = m_fragments[i]; 171 172 ASSERT(fragment.first.is8Bit()); 173 ASSERT(fragment.second.is8Bit()); 174 175 // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#naming-time 176 // Temporal clipping is denoted by the name t, and specified as an interval with a begin 177 // time and an end time 178 if (fragment.first != "t") 179 continue; 180 181 // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#npt-time 182 // Temporal clipping can be specified either as Normal Play Time (npt) RFC 2326, as SMPTE timecodes, 183 // SMPTE, or as real-world clock time (clock) RFC 2326. Begin and end times are always specified 184 // in the same format. The format is specified by name, followed by a colon (:), with npt: being 185 // the default. 186 187 double start = MediaPlayer::invalidTime(); 188 double end = MediaPlayer::invalidTime(); 189 if (parseNPTFragment(fragment.second.characters8(), fragment.second.length(), start, end)) { 190 m_startTime = start; 191 m_endTime = end; 192 m_timeFormat = NormalPlayTime; 193 194 // Although we have a valid fragment, don't return yet because when a fragment dimensions 195 // occurs multiple times, only the last occurrence of that dimension is used: 196 // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#error-uri-general 197 // Multiple occurrences of the same dimension: only the last valid occurrence of a dimension 198 // (e.g., t=10 in #t=2&t=10) is interpreted, all previous occurrences (valid or invalid) 199 // SHOULD be ignored by the UA. 200 } 201 } 202 m_fragments.clear(); 203} 204 205bool MediaFragmentURIParser::parseNPTFragment(const LChar* timeString, unsigned length, double& startTime, double& endTime) 206{ 207 unsigned offset = 0; 208 if (length >= nptIdentiferLength && timeString[0] == 'n' && timeString[1] == 'p' && timeString[2] == 't' && timeString[3] == ':') 209 offset += nptIdentiferLength; 210 211 if (offset == length) 212 return false; 213 214 // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#naming-time 215 // If a single number only is given, this corresponds to the begin time except if it is preceded 216 // by a comma that would in this case indicate the end time. 217 if (timeString[offset] == ',') 218 startTime = 0; 219 else { 220 if (!parseNPTTime(timeString, length, offset, startTime)) 221 return false; 222 } 223 224 if (offset == length) 225 return true; 226 227 if (timeString[offset] != ',') 228 return false; 229 if (++offset == length) 230 return false; 231 232 if (!parseNPTTime(timeString, length, offset, endTime)) 233 return false; 234 235 if (offset != length) 236 return false; 237 238 if (startTime >= endTime) 239 return false; 240 241 return true; 242} 243 244bool MediaFragmentURIParser::parseNPTTime(const LChar* timeString, unsigned length, unsigned& offset, double& time) 245{ 246 enum Mode { minutes, hours }; 247 Mode mode = minutes; 248 249 if (offset >= length || !isASCIIDigit(timeString[offset])) 250 return false; 251 252 // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#npttimedef 253 // Normal Play Time can either be specified as seconds, with an optional 254 // fractional part to indicate miliseconds, or as colon-separated hours, 255 // minutes and seconds (again with an optional fraction). Minutes and 256 // seconds must be specified as exactly two digits, hours and fractional 257 // seconds can be any number of digits. The hours, minutes and seconds 258 // specification for NPT is a convenience only, it does not signal frame 259 // accuracy. The specification of the "npt:" identifier is optional since 260 // NPT is the default time scheme. This specification builds on the RTSP 261 // specification of NPT RFC 2326. 262 // 263 // ; defined in RFC 2326 264 // npt-sec = 1*DIGIT [ "." *DIGIT ] ; definitions taken 265 // npt-hhmmss = npt-hh ":" npt-mm ":" npt-ss [ "." *DIGIT] ; from RFC 2326 266 // npt-mmss = npt-mm ":" npt-ss [ "." *DIGIT] 267 // npt-hh = 1*DIGIT ; any positive number 268 // npt-mm = 2DIGIT ; 0-59 269 // npt-ss = 2DIGIT ; 0-59 270 271 String digits1 = collectDigits(timeString, length, offset); 272 int value1 = digits1.toInt(); 273 if (offset >= length || timeString[offset] == ',') { 274 time = value1; 275 return true; 276 } 277 278 double fraction = 0; 279 if (timeString[offset] == '.') { 280 if (offset == length) 281 return true; 282 String digits = collectFraction(timeString, length, offset); 283 fraction = digits.toDouble(); 284 time = value1 + fraction; 285 return true; 286 } 287 288 if (digits1.length() < 2) 289 return false; 290 if (digits1.length() > 2) 291 mode = hours; 292 293 // Collect the next sequence of 0-9 after ':' 294 if (offset >= length || timeString[offset++] != ':') 295 return false; 296 if (offset >= length || !isASCIIDigit(timeString[(offset)])) 297 return false; 298 String digits2 = collectDigits(timeString, length, offset); 299 int value2 = digits2.toInt(); 300 if (digits2.length() != 2) 301 return false; 302 303 // Detect whether this timestamp includes hours. 304 int value3; 305 if (mode == hours || (offset < length && timeString[offset] == ':')) { 306 if (offset >= length || timeString[offset++] != ':') 307 return false; 308 if (offset >= length || !isASCIIDigit(timeString[offset])) 309 return false; 310 String digits3 = collectDigits(timeString, length, offset); 311 if (digits3.length() != 2) 312 return false; 313 value3 = digits3.toInt(); 314 } else { 315 value3 = value2; 316 value2 = value1; 317 value1 = 0; 318 } 319 320 if (offset < length && timeString[offset] == '.') 321 fraction = collectFraction(timeString, length, offset).toDouble(); 322 323 time = (value1 * secondsPerHour) + (value2 * secondsPerMinute) + value3 + fraction; 324 return true; 325} 326 327} 328#endif 329