Playing an M3U8 playlist as a local file
#1
Hi, sharing some info. I wasn't getting Kodi to play an M3U8 (HLS) playlist that I put in my HDD. The file was named "playlist.m3u8" and looked something like this, with all the segments:
Code:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:14
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:9.924600,
https://hostname.com/path/to/segment-000.ts
#EXTINF:5.504400,
https://hostname.com/path/to/segment-001.ts
#EXTINF:4.962300,
https://hostname.com/path/to/segment-002.ts
#EXTINF:1.042500,
https://hostname.com/path/to/segment-003.ts
(...)
I stored it in a place like "special://temp/myPlaylist.m3u8", and it wouldn't play using either xbmc.Player().play() or xbmcplugin.setResolvedUrl().

But then I found out that using another playlist, a parent playlist that pointed to it, would let Kodi play it. The parent playlist can be stored somewhere like "special://temp/myParentPlaylist.m3u8" and look like this:
Code:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:PROGRAM-ID=1
C:/path_to_Kodi_temp/myPlaylist.m3u8
You can find the path to Kodi's temp by translating the path with xbmc.translatePath("special://temp/myPlaylist.m3u8"), and then writing that to the parent playlist.
And then you play the parent playlist, not the child.

It's useful to know in case you're trying to do something like this.
Reply
#2
Works perfectly. Thanks for sharing this gem Nod
Kodi 21 Windows 10 and 11 | 21 Xbox One X | 21 Linux Mint Virginia XFCE | CoreELEC NO 21 nightly S905X4 aarch64
Reply
#3
An update: it's possible to send the M3U8 playlist (or whatever other content) to Kodi via a socket right after it starts playing an item, instead of writing a file. 
This keeps everything in memory without the need to create a temporary file.
Since we know that Kodi (or rather, FFMpeg or InputStream.Adaptive) sends out a GET request for the content, we can implement this via a simple socket instead of having to import and use a full server library that supports the entire HTTP specification.

This was tested on Kodi 18.9 Leia, with Python 2. I believe it works in Python 3 all the same.
The usage is this: 
python:
manifestContent = ... # Your text data, a UTF-8 string.
serveLocation = '192.168.1.27' # Local IP of your device.
servePort = 8081
item.setPath('http://%s:%d/manifest.mpd' % (serveLocation, servePort))

with ServeOnceManager(manifestContent, 'application/dash+xml', serveLocation, servePort):
    xbmcplugin.setResolvedUrl(PLUGIN_ID, True, item)

The ServeOnceManager class and the standard import needed is this:
python:

import socket

# A context manager for use with Kodi playback, to serve some simple text in
# memory to the Kodi player (which sends a GET request) and then stops serving.
# Can be used for MPEG-DASH .MPD manifests, HLS .M3U8 master playlists etc.
#
# Used like this, together with xbmcplugin.setResolvedUrl():
#serveLocation = '192.168.1.27' # Local IP of your device.
#servePort = 8081
#item.setPath('http://%s:%d/manifest.mpd' % (serveLocation, servePort))
#with ServeOnceManager(manifestContent, 'application/dash+xml',
#                          serveLocation, servePort):
#    xbmcplugin.setResolvedUrl(PLUGIN_ID, True, item)
class ServeOnceManager(object):
    def __init__(self, text, mimeType, hostStr, portInt):
        self.text = text
        self.mimeType = mimeType
        self.hostStr = hostStr
        try:
            self.portInt = int(portInt)
        except:
            raise Exception('Expected an int for portInt, like 8080, '
                            'instead got ' + str(type(portInt)))

    def __enter__(self, *args, **kwargs):
        serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        serverSocket.settimeout(1.0)
        try:
            serverSocket.bind((self.hostStr, self.portInt))
            serverSocket.listen(1)
            self.serverSocket = serverSocket
        except Exception as e:
            raise Exception('Failed to serve on "%s:%d"'
                            % (self.hostStr, self.portInt), e)

def __exit__(self, *args, **kwargs):
connection = None
        attempts = 5
while attempts:
try:
connection, clientAddress = self.serverSocket.accept()
connection.settimeout(1.0)
pieces = []
subAttempts = 5
while subAttempts:
piece = connection.recv(8192)
pieces.append(piece)
if b'\r\n\r\n' in piece:
# Body separator present, the incoming message is complete.
break
elif piece:
subAttempts -= 1
else:
raise Exception('Incomplete response')
message = b''.join(pieces)
if not message:
raise Exception('Empty response')
try:
if message.startswith(b'GET '):
rawText = self.text.encode('utf-8')
# Copied from StreamCatcher.
rawResponse = (
b'HTTP/1.1 200 OK\r\n' # Response status.
b'Allow: GET\r\n' # Acceptable request commands.
#b'Date: {date}\r\n' # HTTP-formatted date.
b'Server: Web/0.1\r\n'
b'Connection: close\r\n'
b'Content-Type: {mimeType}\r\n'
b'Content-Length: {length}\r\n' # Length in bytes.
b'\r\n{body}' # Body of the response, if any.
).format(mimeType=self.mimeType, length=len(rawText),
body=rawText)
connection.sendall(rawResponse)
else:
rawResponse = (b'HTTP/1.1 404 Not Found'
b'Content-Length: 13\r\n'
b'Content-Type: text/plain; '
b'charset=utf-8\r\n'
b'\r\n'
b'404 Not Found')
connection.sendall(rawResponse)
break
except Exception as e:
raise Exception('Error during sendall()', e)
except socket.timeout as e:
pass


        if connection:
            try:
                connection.shutdown(socket.SHUT_RDWR)
                connection.close()
            except:
                pass
        try:
            self.serverSocket.close()
        except Exception as e:
            raise Exception('Failed to close the server')
PS the "attempts" and "subAttempts" looping logic is just a precaution. In all my tests, the connection was formed right on the first attempt.
Reply

Logout Mark Read Team Forum Stats Members Help
Playing an M3U8 playlist as a local file0