Roku Developer Program

Join our online forum to talk to Roku developers and fellow channel creators. Ask questions, share tips with the community, and find helpful resources.
cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 
belltown
Roku Guru

Re: Multiple streams in one channel

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.
0 Kudos
mrattitude
Channel Surfer

Re: Multiple streams in one channel

Your code is exactly what I required, I have been trying to figure it out myself for over a month with little success.

Thank you so much, I really appreciate your input - I will add to the code to suit.

Thanks again.
0 Kudos
TheEndless
Channel Surfer

Re: Multiple streams in one channel

I still maintain that that's not the correct usage of the M3U8 format and is actually an M3U playlist. Using an M3U8 extension just confuses the issue. Since you said you had already put it into a JSON format, it makes a lot more sense to go that route, and simplifies that parsing code that belltown provided down to:
indexList = ParseJson(jsonPlaylistFile)
My Channels: http://roku.permanence.com - Twitter: @TheEndlessDev
Instant Watch Browser (NetflixIWB), Aquarium Screensaver (AQUARIUM), Clever Clocks Screensaver (CLEVERCLOCKS), iTunes Podcasts (ITPC), My Channels (MYCHANNELS)
0 Kudos
belltown
Roku Guru

Re: Multiple streams in one channel

"TheEndless" wrote:
I still maintain that that's not the correct usage of the M3U8 format and is actually an M3U playlist. Using an M3U8 extension just confuses the issue. Since you said you had already put it into a JSON format, it makes a lot more sense to go that route, and simplifies that parsing code that belltown provided down to:
indexList = ParseJson(jsonPlaylistFile)

The only difference between an M3U and an M3U8 playlist is the encoding; an M3U file is encoded as US-ASCII (or Latin-1 or WIndows-1252 depending on which reference you're using), whereas an M3U8 file is encoded in UTF-8.

A "Basic" M3U playlist (or M3U8 playlist if UTF-8 encoded) only contains a list of media resources, and maybe comments or blank lines.

An "Extended" M3U playlist (or M3U8 playlist if UTF-8 encoded), starts with the #EXTM3U tag to signify use of the extended format, and contains other tags, e.g. #EXTINF to provide extended information about a resource, and the resource paths themselves.

The HTTP Live Streaming specification chose to use the "Extended" playlist format for its implementation, for example to define media playlists containing a list of media segments. The HLS spec states that playlist files must be encoded in UTF-8 (so most files use the .m3u8 extension, although the .m3u file extension is allowed for compatibility).

The HLS spec is just one usage of extended M3U8 playlists (and possibly the most common usage -- and who knows, perhaps even the only usage so far on the Roku platform). It is not necessarily the only way to use extended M3U8 playlists. I would contend that it's equally "correct" to use extended M3U8 playlists for any type of media resource, whether they are mp3 audio resources (which is what they were originally designed for), or video file resources (e.g. a playlist of mp4 videos), or a list of HLS media playlists (as used in mrattitude's case).

And since someone's obviously already come up with an M3U8 implementation used as a playlist of separate HLS resources, and mrattitude wants to access that from a Roku channel, I don't see anything incorrect about that.

Still, I don't think that any form of M3U8 playlists are necessarily the most appropriate way to define a list of media items on the Roku platform, and I don't quite understand mrattitude's statement that, "The JSON method worked on a small sample of links (10) BUT is not appropriate for larger lists, especially if the links are prone to change, a playlist is exactly what is required." It shouldn't make any difference what format you're using for a playlist (Json, Xml, M3U/M3U8, PLS, SMIL, plain text, etc. etc.) Whatever format you use, you have some code (format-dependent) that parses the playlist to extract the list of media items, then some other code (format-independent) to select and play them. It shouldn't make any difference whether there are 10 items or 10,000 items. As far as the links being "prone to change", again it shouldn't make any difference what format you use, you'll still have to update your playlist (the Json, M3U8, whatever) whenever the links change.
0 Kudos
TheEndless
Channel Surfer

Re: Multiple streams in one channel

"belltown" wrote:
The only difference between an M3U and an M3U8 playlist is the encoding; an M3U file is encoded as US-ASCII (or Latin-1 or WIndows-1252 depending on which reference you're using), whereas an M3U8 file is encoded in UTF-8.

A "Basic" M3U playlist (or M3U8 playlist if UTF-8 encoded) only contains a list of media resources, and maybe comments or blank lines.

An "Extended" M3U playlist (or M3U8 playlist if UTF-8 encoded), starts with the #EXTM3U tag to signify use of the extended format, and contains other tags, e.g. #EXTINF to provide extended information about a resource, and the resource paths themselves.

The HTTP Live Streaming specification chose to use the "Extended" playlist format for its implementation, for example to define media playlists containing a list of media segments. The HLS spec states that playlist files must be encoded in UTF-8 (so most files use the .m3u8 extension, although the .m3u file extension is allowed for compatibility).

The HLS spec is just one usage of extended M3U8 playlists (and possibly the most common usage -- and who knows, perhaps even the only usage so far on the Roku platform). It is not necessarily the only way to use extended M3U8 playlists. I would contend that it's equally "correct" to use extended M3U8 playlists for any type of media resource, whether they are mp3 audio resources (which is what they were originally designed for), or video file resources (e.g. a playlist of mp4 videos), or a list of HLS media playlists (as used in mrattitude's case).

I understand that, but M3U8 is used almost exclusively for HLS streams, and there's nothing in mrattitude's playlist that requires UTF-8. Using one to list different HLS streams will only serve to confuse anyone who looks at it. If no one else will ever look at the code, then it's not a big deal (unless you want to confuse people on a public forum where you're asking for help).

"belltown" wrote:
And since someone's obviously already come up with an M3U8 implementation used as a playlist of separate HLS resources, and mrattitude wants to access that from a Roku channel, I don't see anything incorrect about that.

Still, I don't think that any form of M3U8 playlists are necessarily the most appropriate way to define a list of media items on the Roku platform, and I don't quite understand mrattitude's statement that, "The JSON method worked on a small sample of links (10) BUT is not appropriate for larger lists, especially if the links are prone to change, a playlist is exactly what is required." It shouldn't make any difference what format you're using for a playlist (Json, Xml, M3U/M3U8, PLS, SMIL, plain text, etc. etc.) Whatever format you use, you have some code (format-dependent) that parses the playlist to extract the list of media items, then some other code (format-independent) to select and play them. It shouldn't make any difference whether there are 10 items or 10,000 items. As far as the links being "prone to change", again it shouldn't make any difference what format you use, you'll still have to update your playlist (the Json, M3U8, whatever) whenever the links change.

Yes, this was the point I was trying to make. He already converted it to JSON, which seems like a much more appropriate format to use for this application. His insistence on using M3U8 to define individual items in a Roku application, and the fact that he was actually using the M3U8 to list separate HLS streams, is what lead me to believe he didn't fully understand the usage. Regardless, as I stated before, he has to parse it programmatically either way, so it's entirely up to him as to how he does it. I just feel that parsing a json string is much less error prone than writing a custom M3U(8) parser in BrightScript, and as you mentioned, provides a lot more flexibility with respect to metadata.
My Channels: http://roku.permanence.com - Twitter: @TheEndlessDev
Instant Watch Browser (NetflixIWB), Aquarium Screensaver (AQUARIUM), Clever Clocks Screensaver (CLEVERCLOCKS), iTunes Podcasts (ITPC), My Channels (MYCHANNELS)
0 Kudos
belltown
Roku Guru

Re: Multiple streams in one channel

Agreed.

A better solution than M3U8 would probably be to convert the playlist file to an equivalent Json playlist file, e.g:


{"playlist":
[
{"title": "Channel One", "length": 0, "path": "http://www.playon.tv/online/iphone5/main.m3u8"},
{"title": "Channel Two", "length": 0, "path": "http://www.nasa.gov/multimedia/nasatv/NTV-Public-IPS.m3u8"},
{"title": "Channel Three", "length": 0, "path": "http://qthttp.apple.com.edgesuite.net/1010qwoeiuryfg/sl.m3u8"},
{"title": "Channel Four", "length": 0, "path": "http://devimages.apple.com/iphone/samples/bipbop/gear1/prog_index.m3u8"},
{"title": "Channel Five", "length": 0, "path": "http://184.72.239.149/vod/smil:BigBuckBunny.smil/playlist.m3u8"}
]
}


And re-write the parseIndex function as:


Function parseIndex (indexString As String) As Object
m3u8List = []
parsedString = parseJson (indexString)
If parsedString <> Invalid
For Each item In parsedString.playlist
m3u8List.Push ({
Title: item.title,
Length: item.length,
ShortDescriptionLine1: item.title,
ShortDescriptionLine2: item.path,
Stream: {Url: item.path}
StreamFormat: "hls"
SwitchingStrategy: "full-adaptation"
})
End For
EndIf
Return m3u8List
End Function
0 Kudos
mrattitude
Channel Surfer

Re: Multiple streams in one channel

Hi Guys,

There is quite a bit of debate going on in regards to my preference of playlists - I use M3U8 playlists with Plex Media Server, they are already up and running, so that is the ONLY reason I wanted to stick to that format.

As I already stated, Belltown has created exactly what I required, it may be dirty, it may be slightly goofy BUT its going to suit me 100% - so a great job done. Thanks again Belltown.

Having read through the code I seriously doubt I could have got it working this year, if at all.

I have tried out Belltown's JSON example and its working perfectly and I agree, if I was compiling new playlists then I would have gone down the JSON route but the playlists are already working for Plex and I want a "port" to Roku... its perfect.

Thank you so much for all the input - that is what a forum is supposed to be all about and in my case the result could not have had a better conclusion.
0 Kudos
bkc302
Visitor

Re: Multiple streams in one channel

This works really well, if using the json parsing, how would I add a image url to each channel? thanks
0 Kudos
rcarlson29
Visitor

Re: Multiple streams in one channel

I just need to find an example xml playlist with two video streams (mp4 or hls - doesnt matter) that play in order.

Every single example I have found has 1 media asset per channel. I would like two.

Thank you
0 Kudos
arjun_thomas
Visitor

Re: Multiple streams in one channel

"mrattitude" wrote:
Your code is exactly what I required, I have been trying to figure it out myself for over a month with little success.

Thank you so much, I really appreciate your input - I will add to the code to suit.

Thanks again.


Hi, did you get this to work? Can someone help me? I'm trying to do the same thing but with a m3u file as feed. I basically want a adfree m3u black with my links as default.
0 Kudos
Need Assistance?
Welcome to the Roku Community! Feel free to search our Community for answers or post your question to get help.

Become a Roku Streaming Expert!

Share your expertise, help fellow streamers, and unlock exclusive rewards as part of the Roku Community. Learn more.