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 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 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 nptIdentifierLength = 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
73MediaFragmentURIParser::MediaFragmentURIParser(const URL& url)
74 : m_url(url)
75 , m_timeFormat(None)
76 , m_startTime(MediaTime::invalidTime())
77 , m_endTime(MediaTime::invalidTime())
78{
79}
80
81MediaTime MediaFragmentURIParser::startTime()
82{
83 if (!m_url.isValid())
84 return MediaTime::invalidTime();
85 if (m_timeFormat == None)
86 parseTimeFragment();
87 return m_startTime;
88}
89
90MediaTime MediaFragmentURIParser::endTime()
91{
92 if (!m_url.isValid())
93 return MediaTime::invalidTime();
94 if (m_timeFormat == None)
95 parseTimeFragment();
96 return m_endTime;
97}
98
99void MediaFragmentURIParser::parseFragments()
100{
101 if (!m_url.hasFragmentIdentifier())
102 return;
103 String fragmentString = m_url.fragmentIdentifier();
104 if (fragmentString.isEmpty())
105 return;
106
107 unsigned offset = 0;
108 unsigned end = fragmentString.length();
109 while (offset < end) {
110 // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#processing-name-value-components
111 // 1. Parse the octet string according to the namevalues syntax, yielding a list of
112 // name-value pairs, where name and value are both octet string. In accordance
113 // with RFC 3986, the name and value components must be parsed and separated before
114 // percent-encoded octets are decoded.
115 size_t parameterStart = offset;
116 size_t parameterEnd = fragmentString.find('&', offset);
117 if (parameterEnd == notFound)
118 parameterEnd = end;
119
120 size_t equalOffset = fragmentString.find('=', offset);
121 if (equalOffset == notFound || equalOffset > parameterEnd) {
122 offset = parameterEnd + 1;
123 continue;
124 }
125
126 // 2. For each name-value pair:
127 // a. Decode percent-encoded octets in name and value as defined by RFC 3986. If either
128 // name or value are not valid percent-encoded strings, then remove the name-value pair
129 // from the list.
130 String name = decodeURLEscapeSequences(fragmentString.substring(parameterStart, equalOffset - parameterStart));
131 String value;
132 if (equalOffset != parameterEnd)
133 value = decodeURLEscapeSequences(fragmentString.substring(equalOffset + 1, parameterEnd - equalOffset - 1));
134
135 // b. Convert name and value to Unicode strings by interpreting them as UTF-8. If either
136 // name or value are not valid UTF-8 strings, then remove the name-value pair from the list.
137 bool validUTF8 = false;
138 if (!name.isEmpty() && !value.isEmpty()) {
139 name = name.utf8(StrictConversion).data();
140 validUTF8 = !name.isEmpty();
141
142 if (validUTF8) {
143 value = value.utf8(StrictConversion).data();
144 validUTF8 = !value.isEmpty();
145 }
146 }
147
148 if (validUTF8)
149 m_fragments.append(std::make_pair(name, value));
150
151 offset = parameterEnd + 1;
152 }
153}
154
155void MediaFragmentURIParser::parseTimeFragment()
156{
157 ASSERT(m_timeFormat == None);
158
159 if (m_fragments.isEmpty())
160 parseFragments();
161
162 m_timeFormat = Invalid;
163
164 for (auto& fragment : m_fragments) {
165 ASSERT(fragment.first.is8Bit());
166 ASSERT(fragment.second.is8Bit());
167
168 // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#naming-time
169 // Temporal clipping is denoted by the name t, and specified as an interval with a begin
170 // time and an end time
171 if (fragment.first != "t")
172 continue;
173
174 // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#npt-time
175 // Temporal clipping can be specified either as Normal Play Time (npt) RFC 2326, as SMPTE timecodes,
176 // SMPTE, or as real-world clock time (clock) RFC 2326. Begin and end times are always specified
177 // in the same format. The format is specified by name, followed by a colon (:), with npt: being
178 // the default.
179
180 MediaTime start = MediaTime::invalidTime();
181 MediaTime end = MediaTime::invalidTime();
182 if (parseNPTFragment(fragment.second.characters8(), fragment.second.length(), start, end)) {
183 m_startTime = start;
184 m_endTime = end;
185 m_timeFormat = NormalPlayTime;
186
187 // Although we have a valid fragment, don't return yet because when a fragment dimensions
188 // occurs multiple times, only the last occurrence of that dimension is used:
189 // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#error-uri-general
190 // Multiple occurrences of the same dimension: only the last valid occurrence of a dimension
191 // (e.g., t=10 in #t=2&t=10) is interpreted, all previous occurrences (valid or invalid)
192 // SHOULD be ignored by the UA.
193 }
194 }
195 m_fragments.clear();
196}
197
198bool MediaFragmentURIParser::parseNPTFragment(const LChar* timeString, unsigned length, MediaTime& startTime, MediaTime& endTime)
199{
200 unsigned offset = 0;
201 if (length >= nptIdentifierLength && timeString[0] == 'n' && timeString[1] == 'p' && timeString[2] == 't' && timeString[3] == ':')
202 offset += nptIdentifierLength;
203
204 if (offset == length)
205 return false;
206
207 // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#naming-time
208 // If a single number only is given, this corresponds to the begin time except if it is preceded
209 // by a comma that would in this case indicate the end time.
210 if (timeString[offset] == ',')
211 startTime = MediaTime::zeroTime();
212 else {
213 if (!parseNPTTime(timeString, length, offset, startTime))
214 return false;
215 }
216
217 if (offset == length)
218 return true;
219
220 if (timeString[offset] != ',')
221 return false;
222 if (++offset == length)
223 return false;
224
225 if (!parseNPTTime(timeString, length, offset, endTime))
226 return false;
227
228 if (offset != length)
229 return false;
230
231 if (startTime >= endTime)
232 return false;
233
234 return true;
235}
236
237bool MediaFragmentURIParser::parseNPTTime(const LChar* timeString, unsigned length, unsigned& offset, MediaTime& time)
238{
239 enum Mode { minutes, hours };
240 Mode mode = minutes;
241
242 if (offset >= length || !isASCIIDigit(timeString[offset]))
243 return false;
244
245 // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#npttimedef
246 // Normal Play Time can either be specified as seconds, with an optional
247 // fractional part to indicate milliseconds, or as colon-separated hours,
248 // minutes and seconds (again with an optional fraction). Minutes and
249 // seconds must be specified as exactly two digits, hours and fractional
250 // seconds can be any number of digits. The hours, minutes and seconds
251 // specification for NPT is a convenience only, it does not signal frame
252 // accuracy. The specification of the "npt:" identifier is optional since
253 // NPT is the default time scheme. This specification builds on the RTSP
254 // specification of NPT RFC 2326.
255 //
256 // ; defined in RFC 2326
257 // npt-sec = 1*DIGIT [ "." *DIGIT ] ; definitions taken
258 // npt-hhmmss = npt-hh ":" npt-mm ":" npt-ss [ "." *DIGIT] ; from RFC 2326
259 // npt-mmss = npt-mm ":" npt-ss [ "." *DIGIT]
260 // npt-hh = 1*DIGIT ; any positive number
261 // npt-mm = 2DIGIT ; 0-59
262 // npt-ss = 2DIGIT ; 0-59
263
264 String digits1 = collectDigits(timeString, length, offset);
265 int value1 = digits1.toInt();
266 if (offset >= length || timeString[offset] == ',') {
267 time = MediaTime::createWithDouble(value1);
268 return true;
269 }
270
271 MediaTime fraction;
272 if (timeString[offset] == '.') {
273 if (offset == length)
274 return true;
275 String digits = collectFraction(timeString, length, offset);
276 fraction = MediaTime::createWithDouble(digits.toDouble());
277 time = MediaTime::createWithDouble(value1) + fraction;
278 return true;
279 }
280
281 if (digits1.length() < 2)
282 return false;
283 if (digits1.length() > 2)
284 mode = hours;
285
286 // Collect the next sequence of 0-9 after ':'
287 if (offset >= length || timeString[offset++] != ':')
288 return false;
289 if (offset >= length || !isASCIIDigit(timeString[(offset)]))
290 return false;
291 String digits2 = collectDigits(timeString, length, offset);
292 int value2 = digits2.toInt();
293 if (digits2.length() != 2)
294 return false;
295
296 // Detect whether this timestamp includes hours.
297 int value3;
298 if (mode == hours || (offset < length && timeString[offset] == ':')) {
299 if (offset >= length || timeString[offset++] != ':')
300 return false;
301 if (offset >= length || !isASCIIDigit(timeString[offset]))
302 return false;
303 String digits3 = collectDigits(timeString, length, offset);
304 if (digits3.length() != 2)
305 return false;
306 value3 = digits3.toInt();
307 } else {
308 value3 = value2;
309 value2 = value1;
310 value1 = 0;
311 }
312
313 if (offset < length && timeString[offset] == '.')
314 fraction = MediaTime::createWithDouble(collectFraction(timeString, length, offset).toDouble());
315
316 time = MediaTime::createWithDouble((value1 * secondsPerHour) + (value2 * secondsPerMinute) + value3) + fraction;
317 return true;
318}
319
320}
321#endif
322