Your custom m3u8 list contains mpd streams. Now what?

Here is an app to stream MPEG-DASH MPD (Media Presentation Description) streams in Channels. Requires no configuration.

The MPDs can be provided directly or as an m3u8 list, non MPD urls in m3u8 lists are left unchanged

Requires python modules flask and streamlink, and ffmpeg binary in the path

Tested with python 3.11 on Debian 12

flask app
from flask import Flask, request, Response
import streamlink
import requests
import re

app = Flask(__name__)


def modify_m3u8_content(content):
    def replace_mpd_urls(line):
        if line.startswith("http") and ".mpd" in line:
            scheme = request.scheme
            host = request.host
            return re.sub(r'^(https?://[^\s]+\.mpd)', lambda match: f'{scheme}://{host}/stream?url={match.group(1)}', line)
        return line

    return "\n".join(replace_mpd_urls(line) for line in content.splitlines())

@app.route('/playlist.m3u8')
def get_custom_m3u8():
    url = request.args.get('url')
    if not url:
        return "Error: No URL provided", 400
    response = requests.get(url)
    if response.status_code != 200:
        return "Error fetching M3U8 playlist", 500
    modified_content = modify_m3u8_content(response.text)
    return Response(modified_content, content_type='application/vnd.apple.mpegurl')

def generate_stream(url):
    session = streamlink.Streamlink()
    streams = session.streams(url)
    
    if "best" not in streams:
        raise ValueError("No valid stream found.")

    stream = streams["best"]
    fd = stream.open()

    try:
        while True:
            chunk = fd.read(8192)
            if not chunk:
                break
            yield chunk
    finally:
        fd.close()

@app.route('/')
def home():
    return '''<h1>Streamlink Flask App</h1>
              <form action="/stream" method="get">
                <label for="url">Enter URL:</label>
                <input type="text" id="url" name="url" required>
                <button type="submit">Stream</button>
              </form>'''

@app.route('/stream')
def stream():
    url = request.args.get('url')
    if not url:
        return "Error: No URL provided", 400
    
    try:
        return Response(generate_stream(url), content_type='video/mp4')
    except ValueError as e:
        return f"Error: {e}", 400

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

also available as https://pastebin.com/eTBNbKQc

maybe it could be dockerized (put under WSGI) @bnhf

Let's say your favorite channel is in mpd format.

For example if your station is
Korean Central Television (KCTV) (576p)
which uses an MPD stream:
https://tv.nknews.org/tvdash/stream.mpd
contained in this M3U8 playlist:
https://iptv-org.github.io/iptv/countries/kp.m3u

Fire the app locally or elsewhere. For this example let's assume the app is running locally on the default port 5000. Prepend the original M3U8 url with

http://localhost:5000/playlist.m3u8?url=
and put this as the playlist source in custom streams

http://localhost:5000/playlist.m3u8?url=https://iptv-org.github.io/iptv/countries/kp.m3u

also set the type to MPEG -TS. You are done. The lists are identical except for the fact the MPD streams work.

There is also a home page to help stream single MPD streams.

it uses the /stream endpoint. E.g. for the original MPD

http://APP_IP:5000/stream?url=https://tv.nknews.org/tvdash/stream.mpd

Flask App

2 Likes