Skip to content

How to provide custom manifest and license

Stefano Gottardo edited this page Oct 21, 2023 · 8 revisions

This can be useful when you need to manipulate manifests and/or licenses, or when it is not possible to get the manifests or licenses from a direct web url and you need to make them yourself. In this case, a proxy can be implemented as an intermediary between ISAdaptive and your video service server.

Prerequisites

All this is possible by implementing a proxy service in your python add-on (ofc you can also use other ways), then you need:

  • Implement a service add-on instance (Kodi service add-ons Wiki)
  • Implement an HTTP server (running on localhost inside the add-on service instance)

Working scheme to understand the order of things

  1. User play a video, Kodi starts ISAdaptive
  2. ISA ask the manifest to your proxy endpoint
  3. Your proxy does the magic to provide the manifest content (e.g. DASH) and return it to the ISA as HTTP response
  4. (If set) The ISA now ask for a license to your proxy endpoint
  5. Your HTTP server does the magic to create the license content and return it to the ISA as HTTP response
  6. Now Kodi will play the video by using ISA demuxer

Configure your ListItem to be played

This is explained in Integration page.

The part so far relevant here, are two properties:

  • listitem = xbmcgui.ListItem(path=mpd_url)

This set the address where to get the manifest, the http requests are always of type GET. So the mpd_url must contains the address of your http proxy server and eventually other arguments required for your purposes.

Important: It is strongly recommended, to add to the address as the first directory a folder that uses the name of your add-on (see add-on_name in the example). This allows ISAdaptive to distinguish the add-on that hosts the proxy, because multiple add-ons can use the same address and port.

Example: http://127.0.0.1:{port}/addon_name/manifest?id=234324

  • listitem.setProperty('inputstream.adaptive.license_key', lic_url)

This set the address where to get the license, the http requests are always of type POST. So the lic_url must contains the address of your http proxy server and eventually other arguments required for your purposes.

Important: It is strongly recommended, to add to the address as the first directory a folder that uses the name of your add-on (see add-on_name in the example). This allows ISAdaptive to distinguish the add-on that hosts the proxy, because multiple add-ons can use the same address and port.

Example: http://127.0.0.1:{port}/addon_name/license?id=234324

How to make a python http proxy server

From this example you can understand how to interact with ISA. There are many examples on the net to make a http server, perhaps the best way is implement a custom http server.

The server must be started within the add-on service instance.

If your add-on service runs multiple features you need to enclose the http server within a separate thread.

Important add "content-type" header in the manifest proxy response

To allow ISA detect the manifest type correctly and faster way, you need to add the content-type header in the proxy manifest response. The accepted values to be used are standard, choose the appropriate one for your use case:

  • MPEG DASH manifest: application/dash+xml
  • HLS manifest: application/vnd.apple.mpegurl
  • Smooth Streaming manifest: application/vnd.ms-sstr+xml
try:  # Python 3
    from http.server import BaseHTTPRequestHandler
except ImportError:  # Python 2
    from BaseHTTPServer import BaseHTTPRequestHandler

try:  # Python 3
    from socketserver import TCPServer
except ImportError:  # Python 2
    from SocketServer import TCPServer

try:  # Python 3
    from urllib.parse import unquote
except ImportError:  # Python 2
    from urllib import unquote


class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        """Handle http get requests, used for manifest"""
        path = self.path  # Path with parameters received from request e.g. "/manifest?id=234324"
        print('HTTP GET Request received to {}'.format(path))
        if '/manifest' not in path:
            self.send_response(404)
            self.end_headers()
            return
        try:
            # To obtain the DRM Challenge and the DRM Session ID data to make a licensed manifest request,
            # you must set the ISA property: inputstream.adaptive.pre_init_data, see Integration Wiki page
            #  challenge_base64 = unquote(self.headers['challengeB64'])
            #  sid = self.headers['sessionId']

            # Call your method to do the magic to generate DASH manifest data
            manifest_data = b'my manifest data'
            self.send_response(200)
            self.send_header('content-type', 'application/dash+xml')
            self.end_headers()
            self.wfile.write(manifest_data)
        except Exception:
            self.send_response(500)
            self.end_headers()

    def do_POST(self):
        """Handle http post requests, used for license"""
        path = self.path  # Path with parameters received from request e.g. "/license?id=234324"
        print('HTTP POST Request received to {}'.format(path))
        if '/license' not in path:
            self.send_response(404)
            self.end_headers()
            return
        try:
            # InputStream Adaptive can send some data depending by license_key settings
            # The data is splitted by "!" char
            # This example split 'challenge' and 'session id' data
            length = int(self.headers.get('content-length', 0))
            isa_data = self.rfile.read(length).decode('utf-8').split('!')
            
            challenge = isa_data[0]
            session_id = isa_data[1]
            # Call your method to do the magic to generate license data
            # The format type of data must be correct in according to your VOD service
            license_data = b'my license data'
            self.send_response(200)
            self.end_headers()
            self.wfile.write(license_data)
        except Exception:
            self.send_response(500)
            self.end_headers()

address = '127.0.0.1'  # Localhost
# The port in this example is fixed, DO NOT USE A FIXED PORT!
# Other add-ons, or operating system functionality, or other software may use the same port!
# You have to implement a way to get a random free port
port = 6969
server_inst = TCPServer((address, port), SimpleHTTPRequestHandler)
# The follow line is only for test purpose, you have to implement a way to stop the http service!
server_inst.serve_forever()