You can indeed use an m3u8 playlist file (or any other kind of text file, e.g. Xml, Json, PLS, etc.) to contain a list of media items, where each media item is an m3u8 file for an individual HLS playlist. You just have to parse the m3u8 file to extract the individual HLS m3u8 playlist files, as TheEndless stated earlier.
Most Roku apps tend to use Xml or Json files for this purpose because (a) those formats have built-in native BrightScript parsing functions, and (b) they are more flexible in terms of the metadata you can include to describe your media items.
But if you already have an m3u8 file listing your media items, and don't want to go to the trouble of converting it to Xml or Json, then here's some code that can get you started:
Function _DEBUG () As Boolean : Return True : End Function ' Return False in production code
' Main entry point
Sub Main ()
' Get main playlist index (can specify a local file or a url, and an optional timeout)
indexString = getPathToString ("pkg:/data/index.m3u8")
' Parse the index file returning a list of m3u8 playlist files
indexList = parseIndex (indexString)
' Display the list
displayIndexList (indexList)
End Sub
' Quick 'N Dirty M3U8 Parser
Function parseIndex (indexString As String) As Object
m3u8List = []
reLineSplit = CreateObject ("roRegex", "(?>\r\n|[\r\n])", "")
reExtinf = CreateObject ("roRegex", "^#EXTINF:\s*(\d+|-1)\s*,\s*(.*)$", "")
rePath = CreateObject ("roRegex", "^([^#].*)$", "")
inExtinf = False
For Each line In reLineSplit.Split (indexString)
If inExtinf
maPath = rePath.Match (line)
If maPath.Count () = 2
path = maPath [1]
m3u8List.Push ({
Title: title,
Length: length,
ShortDescriptionLine1: title,
ShortDescriptionLine2: path,
Stream: {Url: path}
StreamFormat: "hls"
SwitchingStrategy: "full-adaptation"
})
inExtinf = False
Endif
Endif
maExtinf = reExtinf.Match (line)
If maExtinf.Count () = 3
length = maExtinf [1].ToInt ()
If length < 0 Then length = 0
title = maExtinf [2]
inExtinf = True
Endif
End For
Return m3u8List
End Function
' Display a list of media items
Function displayIndexList (contentList As Object) As Void
port = CreateObject ("roMessagePort")
ui = CreateObject ("roListScreen")
ui.SetMessagePort (port)
ui.SetContent (contentList)
ui.Show ()
While True
msg = Wait (0, port)
If Type (msg) = "roListScreenEvent"
If msg.IsScreenClosed ()
Exit While
Else If msg.IsListItemSelected ()
index = msg.GetIndex ()
displayItem (contentList [index])
Endif
Endif
End While
End Function
' Play an individual media item
Function displayItem (content As Object) As Void
port = CreateObject ("roMessagePort")
ui = CreateObject ("roVideoScreen")
ui.SetMessagePort (port)
ui.SetContent (content)
ui.Show ()
While True
msg = Wait (0, port)
logEvent ("displayItem", msg)
If Type (msg) = "roVideoScreenEvent"
If msg.IsScreenClosed ()
Exit While
Else If msg.IsFullResult ()
Exit While
Else If msg.IsPartialResult ()
Exit While
Else If msg.IsRequestFailed ()
Exit While
Endif
Endif
End While
End Function
'====== END OF MAIN CODE =========
'===================
' Utility Functions
'===================
'
' Return the contents of a local file as a String
'
Function getFileToString (fileName As String) As String
Return ReadAsciiFile (fileName)
End Function
'
' Issue an HTTP GET request for the specified resource, returning as a String
'
Function getUrlToString (url As String, timeout = 0 As Integer, headers = Invalid As Object) As String
data = ""
port = CreateObject ("roMessagePort")
ut = CreateObject ("roUrlTransfer")
ut.SetPort (port)
ut.SetUrl (url)
If headers <> Invalid
ut.AddHeaders (headers)
Endif
ut.EnableEncodings (True)
https = "https:"
If LCase (Left (url, Len (https))) = https
ut.SetCertificatesFile ("common:/certs/ca-bundle.crt")
ut.AddHeader ("X-Roku-Reserved-Dev-Id", "")
ut.InitClientCertificates ()
Endif
If ut.AsyncGetToString ()
finished = False
While Not finished
msg = Wait (timeout, port)
If msg = Invalid
finished = True
debug ("getUrlToString. AsyncGetToString timed out after " + timeout.ToStr () + " milliseconds")
ut.AsyncCancel ()
Else
logEvent ("getUrlToString", msg)
If Type (msg) = "roUrlEvent"
finished = True
If msg.GetInt () = 1
responseCode = msg.GetResponseCode ()
If responseCode < 0
' cUrl error
Else if responseCode <> 200
' HTTP error
Else
data = msg.GetString ()
Endif
Else
debug ("getUrlToString. AsyncGetToString did not complete")
ut.AsyncCancel ()
Endif
Endif
Endif
End While
Else
debug ("getUrlToString. AsyncGetToString failed")
Endif
Return data
End Function
Function IsPkg (path As String) As Boolean
pkgDevice = "pkg:"
Return LCase (Left (path, Len (pkgDevice))) = pkgDevice
End Function
Function IsTmp (path As String) As Boolean
tmpDevice = "tmp:"
Return LCase (Left (path, Len (tmpDevice))) = tmpDevice
End Function
Function IsCommon (path As String) As Boolean
commonDevice = "common:"
Return LCase (Left (path, Len (commonDevice))) = commonDevice
End Function
Function IsExt (path As String) As Boolean
extDevice = "ext"
Return LCase (Left (path, Len (extDevice))) = extDevice And (Mid (path, 4, 1) >= "0" And Mid (path, 4, 1) <= "9") And Mid (path, 5, 1) = ":"
End Function
Function IsFile (path As String) As Boolean
Return IsPkg (path) Or IsTmp (path) Or IsCommon (path) Or IsExt (path)
End Function
Function getPathToString (path As String, timeout = 0 As Integer) As String
If IsFile (path)
data = getFileToString (path)
Else
data = getUrlToString (path, timeout)
Endif
Return data
End Function
'
' Log events
'
Sub logEvent (proc As String, msg As Dynamic)
If _DEBUG ()
If msg = Invalid
evType = "Invalid"
debug (proc + ". Invalid")
Else
evStr = ""
evType = Type (msg)
If evType = "roVideoScreenEvent" Or evType = "roVideoPlayerEvent"
msgType = msg.GetType ().ToStr ()
If msg.IsStreamStarted ()
info = msg.GetInfo ()
If info.IsUnderrun Then underrun = "true" Else underrun = "false"
evStr = "isStreamStarted. Index: " + msg.GetIndex ().ToStr ()
evStr = evStr + ". Url: " + info.Url
evStr = evStr + ". StreamBitrate: " + info.StreamBitrate.ToStr ()
evStr = evStr + ". MeasuredBitrate: " + info.MeasuredBitrate.ToStr ()
evStr = evStr + ". IsUnderrun: " + underrun
Else If msg.IsPlaybackPosition ()
evStr = "isPlaybackPosition. Index: " + msg.GetIndex ().ToStr ()
Else If msg.IsRequestFailed ()
info = msg.GetInfo ()
evStr = "isRequestFailed. Message: " + msg.GetMessage () + " Index: " + msg.GetIndex ().ToStr ()
If info.LookupCI ("Url") <> Invalid Then evStr = evStr + ". Url: " + info.Url
If info.LookupCI ("StreamBitrate") <> Invalid Then evStr = evStr + ". StreamBitrate: " + info.StreamBitrate.ToStr ()
If info.LookupCI ("MeasuredBitrate") <> Invalid Then evStr = evStr + ". MeasuredBitrate: " + info.MeasuredBitrate.ToStr ()
If info.LookupCI ("MediaFormat") <> Invalid Then evStr = evStr + ". MediaFormat: " + "???"
Else If msg.IsStatusMessage ()
evStr = "isStatusMessage. Message: " + msg.GetMessage ()
Else If msg.IsFullResult ()
evStr = "isFullResult"
Else If msg.IsPartialResult ()
evStr = "isPartialResult"
Else If msg.IsPaused ()
evStr = "isPaused"
Else If msg.IsResumed ()
evStr = "isResumed"
Else If msg.IsScreenClosed ()
evStr = "isScreenClosed"
Else If msg.IsStreamSegmentInfo ()
info = msg.GetInfo ()
evStr = "isStreamSegmentInfo. Message: " + msg.GetMessage () + ". Index: " + msg.GetIndex ().ToStr ()
evStr = evStr + ". StreamBandwidth: " + info.StreamBandwidth.ToStr ()
evStr = evStr + ". Sequence: " + info.Sequence.ToStr ()
evStr = evStr + ". SegUrl: " + info.SegUrl
evStr = evStr + ". SegStartTime: " + info.SegStartTime.ToStr ()
Else If msg.IsDownloadSegmentInfo () ' Undocumented event
info = msg.GetInfo ()
evStr = "isDownloadSegmentInfo. Message: " + msg.GetMessage () + ". Index: " + msg.GetIndex ().ToStr ()
evStr = evStr + ". Sequence: " + info.Sequence.ToStr ()
evStr = evStr + ". Status: " + info.Status.ToStr ()
evStr = evStr + ". SegBitrate: " + info.SegBitrate.ToStr ()
evStr = evStr + ". DownloadDuration: " + info.DownloadDuration.ToStr ()
evStr = evStr + ". SegUrl: " + info.SegUrl
evStr = evStr + ". SegSize: " + info.SegSize.ToStr ()
evStr = evStr + ". BufferSize: " + info.BufferSize.ToStr ()
evStr = evStr + ". BufferLevel: " + info.BufferLevel.ToStr ()
evStr = evStr + ". SegType: " + info.SegType.ToStr ()
Else
evStr = "Unknown. Message: " + msg.GetMessage () + " Index: " + msg.GetIndex ().ToStr ()
Endif
debug (proc + ". " + evType + " [" + msgType + "]-" + evStr)
Else If evType = "roUrlEvent"
evStr = ""
evStr = evStr + ". Int: " + msg.GetInt ().ToStr ()
evStr = evStr + ". ResponseCode: " + msg.GetResponseCode ().ToStr ()
evStr = evStr + ". FailureReason: " + msg.GetFailureReason ()
evStr = evStr + ". SourceIdentity: " + msg.GetSourceIdentity ().ToStr ()
evStr = evStr + ". TargetIpAddress: " + msg.GetTargetIpAddress ()
debug (proc + ". " + evType + evStr)
'
' Add more event types here as needed ...
'
Else
debug (proc + ". Unexpected Event: " + evType)
Endif
Endif
Endif
End Sub
'
' Log debug messages to the console. See _DEBUG () at the start of the code
'
Sub debug (message As String)
If _DEBUG ()
dt = CreateObject ("roDateTime")
dt.ToLocalTime ()
hh = Right ("0" + dt.GetHours ().ToStr (), 2)
mm = Right ("0" + dt.GetMinutes ().ToStr (), 2)
ss = Right ("0" + dt.GetSeconds ().ToStr (), 2)
mmm = Right ("00" + dt.GetMilliseconds ().ToStr (), 3)
Print hh; ":"; mm; ":"; ss; "."; mmm; " "; message
Endif
End Sub
I tested it on this file:
#EXTM3U
#EXTINF:-1,Channel One
http://www.playon.tv/online/iphone5/main.m3u8
#EXTINF:-1,Channel Two
http://www.nasa.gov/multimedia/nasatv/NTV-Public-IPS.m3u8
#EXTINF:-1,Channel Three
http://qthttp.apple.com.edgesuite.net/1010qwoeiuryfg/sl.m3u8
#EXTINF:-1,Channel Four
http://devimages.apple.com/iphone/samples/bipbop/gear1/prog_index.m3u8
#EXTINF:-1,Channel Five
http://184.72.239.149/vod/smil:BigBuckBunny.smil/playlist.m3u8
If you go with a different format other than m3u8 for your playlist file, then re-write the parseIndex function. If you want to list the media items on something other than a list screen then re-write the displayIndexList function.