24 from datetime
import datetime
26 from http.server
import BaseHTTPRequestHandler, HTTPServer
34 startTime = time.time()
37 streamType = StreamType.VOD
39 addDiscontinuities =
False
43 """Get a video test stream HLS playlist modified to emulate an ongoing
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.
51 path -- Playlist file path
54 currentTime = time.time()
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]")
69 with open(path,
"r")
as f:
72 m = extinfPattern.match(line)
75 totalDuration += float(m.group(1))
79 m = extXTargetDurationPattern.match(line)
82 targetDuration = float(m.group(1))
86 with open(path,
"r")
as f:
87 self.send_response(200)
88 self.send_header(
'Access-Control-Allow-Origin',
'*')
93 m = extinfPattern.match(line)
96 segmentDuration = float(m.group(1))
100 if currentPlayTime < (segmentTime + segmentDuration):
106 totalDuration -= segmentDuration
108 (currentPlayTime >= (segmentTime + segmentDuration + self.
liveWindow))
and
109 (totalDuration >= (targetDuration*3.0))):
111 segmentTime += segmentDuration
121 self.wfile.write(bytes(
"#EXT-X-MEDIA-SEQUENCE:%d\n" % (sequenceNumber),
"utf-8"))
127 if sequenceNumber == 0:
128 self.wfile.write(bytes(
"#EXT-X-DISCONTINUITY-SEQUENCE:0\n",
"utf-8"))
130 self.wfile.write(bytes(
"#EXT-X-DISCONTINUITY-SEQUENCE:%d\n" % (sequenceNumber - 1),
"utf-8"))
134 self.wfile.write(bytes(
"#EXT-X-DISCONTINUITY\n",
"utf-8"))
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"))
141 segmentTime += segmentDuration
143 elif extXPlaylistTypePattern.match(line):
146 line =
"#EXT-X-PLAYLIST-TYPE:EVENT\n"
150 elif extXMediaSequencePattern.match(line):
156 elif mediaUrlPattern.match(line):
163 self.wfile.write(bytes(line,
"utf-8"))
166 """Get a video test stream DASH manifest modified to emulate an
169 Later segments are removed from the manifest based on the time since the
173 path -- Manifest file path
176 currentTime = time.time()
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+)\"")
189 with open(path,
"r")
as f:
190 self.send_response(200)
191 end_header(
'Access-Control-Allow-Origin',
'*')
196 m = mpdTypePattern.search(line)
199 line = line.replace(
"\"static\"",
"\"dynamic\"")
202 m = mpdMediaPresentationDurationPattern.search(line)
205 line = mpdMediaPresentationDurationPattern.sub(
"", line)
208 m = mpdTimescalePattern.search(line)
211 timescale = float(m.group(1))
215 m = mpdSegmentPattern.search(line)
218 attributes = m.group(1)
222 m = mpdSegmentDurationPattern.search(attributes)
224 d = float(m.group(1))
228 m = mpdSegmentRepeatPattern.search(attributes)
230 rPlusOne = float(m.group(1)) + 1.0
236 m = mpdSegmentTimePattern.search(attributes)
238 t = float(m.group(1))
239 startSegmentTime = t/timescale
242 startSegmentTime = segmentTime
245 segments = (currentPlayTime - startSegmentTime)/(d/timescale)
246 segmentTime = startSegmentTime + ((rPlusOne*d)/timescale)
250 elif (segments <= rPlusOne):
257 line = mpdSegmentPattern.sub(
"<S t=\"%d\" d=\"%d\" r=\"%d\" />" % (int(t), int(d), int(rPlusOne - 1.0)), line);
259 line = mpdSegmentPattern.sub(
"<S t=\"%d\" d=\"%d\" />" % (int(t), int(d)), line);
262 line = mpdSegmentPattern.sub(
"<S d=\"%d\" r=\"%d\" />" % (int(d), int(rPlusOne - 1.0)), line);
264 line = mpdSegmentPattern.sub(
"<S d=\"%d\" />" % (int(d)), line);
266 self.wfile.write(bytes(line,
"utf-8"))
275 with open(path,
"rb")
as f:
277 self.send_response(200)
278 self.send_header(
'Access-Control-Allow-Origin',
'*')
280 self.wfile.write(contents)
290 filename, extension = os.path.splitext(path)
293 if extension ==
".m3u8":
299 elif extension ==
".mpd":
308 elif extension ==
".m4s":
311 elif extension ==
".mp4":
314 elif extension ==
".ts":
317 elif extension ==
".mp3":
321 self.send_response(404)
324 except FileNotFoundError:
325 self.send_response(404)
328 if __name__ ==
"__main__":
329 hostName =
"localhost"
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()
347 TestServer.streamType = StreamType.EVENT
350 TestServer.streamType = StreamType.LIVE
351 TestServer.addPDTTags =
True
354 TestServer.addPDTTags =
True
356 if args.discontinuity:
357 TestServer.addDiscontinuities =
True
360 serverPort = args.port
363 TestServer.minTime = args.mintime
366 TestServer.liveWindow = args.livewindow
369 TestServer.getAll =
True
372 testServer = HTTPServer((hostName, serverPort), TestServer)
373 print(
"Server started http://%s:%s" % (hostName, serverPort))
376 testServer.serve_forever()
377 except KeyboardInterrupt:
380 testServer.server_close()
381 print(
"Server stopped.")