RDK Documentation (Open Sourced RDK Components)
server.py
1 ##########################################################################
2 # If not stated otherwise in this file or this component's Licenses.txt
3 # file the following copyright and licenses apply:
4 #
5 # Copyright 2022 RDK Management
6 #
7 # Licensed under the Apache License, Version 2.0 (the "License");
8 # you may not use this file except in compliance with the License.
9 # You may obtain a copy of the License at
10 #
11 # http://www.apache.org/licenses/LICENSE-2.0
12 #
13 # Unless required by applicable law or agreed to in writing, software
14 # distributed under the License is distributed on an "AS IS" BASIS,
15 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
18 ##########################################################################
19 
20 import time
21 import re
22 import os
23 import argparse
24 from datetime import datetime
25 from enum import Enum
26 from http.server import BaseHTTPRequestHandler, HTTPServer
27 
28 class StreamType(Enum):
29  VOD = 1
30  EVENT = 2
31  LIVE = 3
32 
33 class TestServer(BaseHTTPRequestHandler):
34  startTime = time.time()
35  minTime = 10.0
36  liveWindow = 30.0
37  streamType = StreamType.VOD
38  addPDTTags = False
39  addDiscontinuities = False
40  getAll = False
41 
42  def getEventHLSPlaylist(self, path):
43  """Get a video test stream HLS playlist modified to emulate an ongoing
44  event or live.
45 
46  Event media playlists are truncated based on the time since the server
47  was started. Live media playlists contain a window of segments based on
48  the time since the server was started. Master playlists are unmodified.
49 
50  self -- Instance
51  path -- Playlist file path
52  """
53 
54  currentTime = time.time()
55  currentPlayTime = (currentTime - self.startTime) + self.minTime
56  segmentTime = 0.0
57  extXTargetDurationPattern = re.compile(r"^#EXT-X-TARGETDURATION:([\d\.]+).*")
58  extXPlaylistTypePattern = re.compile(r"^#EXT-X-PLAYLIST-TYPE:.*")
59  extinfPattern = re.compile(r"^#EXTINF:([\d\.]+),.*")
60  extXMediaSequencePattern = re.compile(r"^#EXT-X-MEDIA-SEQUENCE:.*")
61  mediaUrlPattern = re.compile(r"^[^#\s]")
62  skipSegment = False
63  targetDuration = 0.0
64  totalDuration = 0.0
65  firstSegment = True
66  sequenceNumber = 0
67 
68  # Get the target and total durations
69  with open(path, "r") as f:
70  for line in f:
71  # "^#EXTINF:([\d\.]+),.*"
72  m = extinfPattern.match(line)
73  if m:
74  # Extract the segment duration.
75  totalDuration += float(m.group(1))
76  continue
77 
78  # "^#EXT-X-TARGETDURATION:([\d\.]+).*"
79  m = extXTargetDurationPattern.match(line)
80  if m:
81  # Extract the target duration.
82  targetDuration = float(m.group(1))
83  continue
84 
85  # Get a modified version of the playlist
86  with open(path, "r") as f:
87  self.send_response(200)
88  self.send_header('Access-Control-Allow-Origin', '*')
89  self.end_headers()
90 
91  for line in f:
92  # "^#EXTINF:([\d\.]+),.*"
93  m = extinfPattern.match(line)
94  if m:
95  # Extract the segment duration.
96  segmentDuration = float(m.group(1))
97 
98  # Truncate the playlist if the next segment ends after the
99  # current playlist time.
100  if currentPlayTime < (segmentTime + segmentDuration):
101  break
102 
103  # In live emulation, skip segments outside the live window
104  # unless the total duration would be less than three times
105  # the target duration.
106  totalDuration -= segmentDuration
107  if ((self.streamType == StreamType.LIVE) and
108  (currentPlayTime >= (segmentTime + segmentDuration + self.liveWindow)) and
109  (totalDuration >= (targetDuration*3.0))):
110  skipSegment = True
111  segmentTime += segmentDuration
112  continue
113  else:
114  skipSegment = False
115 
116  # If this is the first segment to be emitted, then emit the
117  # EXT-X-MEDIA-SEQUENCE and EXT-X-DISCONTINUITY-SEQUENCE
118  # tags.
119  if firstSegment:
120  firstSegment = False
121  self.wfile.write(bytes("#EXT-X-MEDIA-SEQUENCE:%d\n" % (sequenceNumber), "utf-8"))
122 
123  if self.addDiscontinuities:
124  # Segment 0 has no EXT-X-DISCONTINUITY flag. If
125  # segment 0 is removed from the playlist, don't
126  # increment the EXT-X-DISCONTINUITY-SEQUENCE value.
127  if sequenceNumber == 0:
128  self.wfile.write(bytes("#EXT-X-DISCONTINUITY-SEQUENCE:0\n", "utf-8"))
129  else:
130  self.wfile.write(bytes("#EXT-X-DISCONTINUITY-SEQUENCE:%d\n" % (sequenceNumber - 1), "utf-8"))
131 
132  if self.addDiscontinuities and sequenceNumber > 0:
133  # Segment 0 has no EXT-X-DISCONTINUITY flag.
134  self.wfile.write(bytes("#EXT-X-DISCONTINUITY\n", "utf-8"))
135 
136  if self.addPDTTags:
137  # Add program date time tag
138  timestring = datetime.fromtimestamp(self.startTime + segmentTime).astimezone().isoformat(timespec='milliseconds')
139  self.wfile.write(bytes("#EXT-X-PROGRAM-DATE-TIME:" + timestring + "\n", "utf-8"))
140 
141  segmentTime += segmentDuration
142  # "^#EXT-X-PLAYLIST-TYPE:.*"
143  elif extXPlaylistTypePattern.match(line):
144  # Skip or replace the playlist type tag
145  if self.streamType == StreamType.EVENT:
146  line = "#EXT-X-PLAYLIST-TYPE:EVENT\n"
147  else:
148  continue
149  # "^#EXT-X-MEDIA-SEQUENCE:.*"
150  elif extXMediaSequencePattern.match(line):
151  # Delay emitting the EXT-X-MEDIA-SEQUENCE tag until the
152  # first segment is about to be emitted when we know what the
153  # first sequence number is.
154  continue
155  # "^[^#\s]"
156  elif mediaUrlPattern.match(line):
157  # Media segment URI.
158  sequenceNumber += 1
159  if skipSegment:
160  skipSegment = False
161  continue
162 
163  self.wfile.write(bytes(line, "utf-8"))
164 
165  def getEventDASHManifest(self, path):
166  """Get a video test stream DASH manifest modified to emulate an
167  ongoing event.
168 
169  Later segments are removed from the manifest based on the time since the
170  server was started.
171 
172  self -- Instance
173  path -- Manifest file path
174  """
175 
176  currentTime = time.time()
177  currentPlayTime = (currentTime - self.startTime) + self.minTime
178  segmentTime = 0.0
179  timescale = 10000.0
180  mpdTypePattern = re.compile(r"type=\"static\"")
181  mpdMediaPresentationDurationPattern = re.compile(r"mediaPresentationDuration=\"PT((\d+H)?)((\d+M)?)(\d+\.?\d+S)\"")
182  mpdTimescalePattern = re.compile(r"timescale=\"([\d]+)\"")
183  mpdSegmentPattern = re.compile(r"<S((\s+\w+=\"\d+\")+)\s*/>")
184  mpdSegmentDurationPattern = re.compile(r"d=\"(\d+)\"")
185  mpdSegmentRepeatPattern = re.compile(r"r=\"(\d+)\"")
186  mpdSegmentTimePattern = re.compile(r"t=\"(\d+)\"")
187 
188  # Get a modified version of the playlist
189  with open(path, "r") as f:
190  self.send_response(200)
191  end_header('Access-Control-Allow-Origin', '*')
192  self.end_headers()
193 
194  for line in f:
195  # "type=\"static\""
196  m = mpdTypePattern.search(line)
197  if m:
198  # Change the manifest type
199  line = line.replace("\"static\"", "\"dynamic\"")
200 
201  # "mediaPresentationDuration=\"PT((\d+H)?)((\d+M)?)(\d+\.?\d+S)\""
202  m = mpdMediaPresentationDurationPattern.search(line)
203  if m:
204  # Remove mediaPresentationDuration for dynamic manifests
205  line = mpdMediaPresentationDurationPattern.sub("", line)
206 
207  # "timescale=\"([\d]+)\""
208  m = mpdTimescalePattern.search(line)
209  if m:
210  # Extract the segment timescale
211  timescale = float(m.group(1))
212  segmentTime = 0.0
213 
214  # "<S((\s+\w+=\"\d+\")+)\s*/>"
215  m = mpdSegmentPattern.search(line)
216  if m:
217  # Segment attributes
218  attributes = m.group(1)
219 
220  # Extract the segment duration attribute
221  # "d=\"(\d+)\""
222  m = mpdSegmentDurationPattern.search(attributes)
223  if m:
224  d = float(m.group(1))
225 
226  # Extract the segment repeat attribute (plus one)
227  # "r=\"(\d+)\""
228  m = mpdSegmentRepeatPattern.search(attributes)
229  if m:
230  rPlusOne = float(m.group(1)) + 1.0
231  else:
232  rPlusOne = 1.0
233 
234  # Extract the time at the start of the segments
235  # "t=\"(\d+)\""
236  m = mpdSegmentTimePattern.search(attributes)
237  if m:
238  t = float(m.group(1))
239  startSegmentTime = t/timescale
240  else:
241  t = None
242  startSegmentTime = segmentTime
243 
244  # Truncate the number of segments if required
245  segments = (currentPlayTime - startSegmentTime)/(d/timescale)
246  segmentTime = startSegmentTime + ((rPlusOne*d)/timescale)
247  if segments < 1.0:
248  # No complete segment
249  continue
250  elif (segments <= rPlusOne):
251  # Truncate the segments
252  rPlusOne = segments
253 
254  # Rewrite the manifest
255  if t is not None:
256  if rPlusOne >= 2.0:
257  line = mpdSegmentPattern.sub("<S t=\"%d\" d=\"%d\" r=\"%d\" />" % (int(t), int(d), int(rPlusOne - 1.0)), line);
258  else:
259  line = mpdSegmentPattern.sub("<S t=\"%d\" d=\"%d\" />" % (int(t), int(d)), line);
260  else:
261  if rPlusOne >= 2.0:
262  line = mpdSegmentPattern.sub("<S d=\"%d\" r=\"%d\" />" % (int(d), int(rPlusOne - 1.0)), line);
263  else:
264  line = mpdSegmentPattern.sub("<S d=\"%d\" />" % (int(d)), line);
265 
266  self.wfile.write(bytes(line, "utf-8"))
267 
268  def getFile(self, path):
269  """Get a file.
270 
271  self -- Instance
272  path -- File path
273  """
274 
275  with open(path, "rb") as f:
276  contents = f.read()
277  self.send_response(200)
278  self.send_header('Access-Control-Allow-Origin', '*')
279  self.end_headers()
280  self.wfile.write(contents)
281 
282  def do_GET(self):
283  """Get request.
284 
285  self -- Instance
286  """
287 
288  # Extract the relative path and file extension
289  path = self.path[1:]
290  filename, extension = os.path.splitext(path)
291 
292  try:
293  if extension == ".m3u8":
294  # HLS playlist
295  if self.streamType == StreamType.VOD:
296  self.getFile(path)
297  else:
298  self.getEventHLSPlaylist(path)
299  elif extension == ".mpd":
300  # DASH manifest
301  if self.streamType == StreamType.EVENT:
302  self.getEventDASHManifest(path)
303  else:
304  self.getFile(path)
305  elif self.getAll:
306  # Get all files
307  self.getFile(path)
308  elif extension == ".m4s":
309  # fMP4 segment
310  self.getFile(path)
311  elif extension == ".mp4":
312  # MP4 segment
313  self.getFile(path)
314  elif extension == ".ts":
315  # MPEG TS segment
316  self.getFile(path)
317  elif extension == ".mp3":
318  # MP3 audio
319  self.getFile(path)
320  else:
321  self.send_response(404)
322  self.end_headers()
323 
324  except FileNotFoundError:
325  self.send_response(404)
326  self.end_headers()
327 
328 if __name__ == "__main__":
329  hostName = "localhost"
330  serverPort = 8080
331 
332  # Parse the command line arguments.
333  parser = argparse.ArgumentParser(description="AAMP video test stream HTTP server")
334  group = parser.add_mutually_exclusive_group()
335  group.add_argument("--vod", action="store_true", help="VOD test stream (default)")
336  group.add_argument("--event", action="store_true", help="emulate an event test stream")
337  group.add_argument("--live", action="store_true", help="emulate a live test stream (HLS only)")
338  parser.add_argument("--time", action="store_true", help="add EXT-X-PROGRAM-DATE-TIME tags to HLS (or live) event playlists (enabled for live)")
339  parser.add_argument("--discontinuity", action="store_true", help="add EXT-X-DISCONTINUITY tags to HLS event playlists")
340  parser.add_argument("--port", type=int, help="HTTP server port number")
341  parser.add_argument("--mintime", type=float, help="starting event (or live) duration in seconds (default %d)" % (TestServer.minTime))
342  parser.add_argument("--livewindow", type=float, help="live window in seconds (default %d)" % (TestServer.liveWindow))
343  parser.add_argument("--all", action="store_true", help="enable GET of all files. By default, only files with expected extensions will be served")
344  args = parser.parse_args()
345 
346  if args.event:
347  TestServer.streamType = StreamType.EVENT
348 
349  if args.live:
350  TestServer.streamType = StreamType.LIVE
351  TestServer.addPDTTags = True
352 
353  if args.time:
354  TestServer.addPDTTags = True
355 
356  if args.discontinuity:
357  TestServer.addDiscontinuities = True
358 
359  if args.port:
360  serverPort = args.port
361 
362  if args.mintime:
363  TestServer.minTime = args.mintime
364 
365  if args.livewindow:
366  TestServer.liveWindow = args.livewindow
367 
368  if args.all:
369  TestServer.getAll = True
370 
371  # Create and run the HTTP server.
372  testServer = HTTPServer((hostName, serverPort), TestServer)
373  print("Server started http://%s:%s" % (hostName, serverPort))
374 
375  try:
376  testServer.serve_forever()
377  except KeyboardInterrupt:
378  pass
379 
380  testServer.server_close()
381  print("Server stopped.")
382 
server.TestServer.addPDTTags
bool addPDTTags
Definition: server.py:38
server.TestServer.liveWindow
float liveWindow
Definition: server.py:36
server.TestServer.minTime
float minTime
Definition: server.py:35
server.TestServer
Definition: server.py:33
server.TestServer.startTime
startTime
Definition: server.py:34
server.TestServer.do_GET
def do_GET(self)
Definition: server.py:282
server.TestServer.streamType
streamType
Definition: server.py:37
server.StreamType
Definition: server.py:28
server.TestServer.getEventHLSPlaylist
def getEventHLSPlaylist(self, path)
Definition: server.py:42
server.TestServer.getEventDASHManifest
def getEventDASHManifest(self, path)
Definition: server.py:165
server.TestServer.getAll
bool getAll
Definition: server.py:40
server.TestServer.addDiscontinuities
bool addDiscontinuities
Definition: server.py:39
server.TestServer.getFile
def getFile(self, path)
Definition: server.py:268