Streaming works, except over iOS App

Hi,

 

I've nearly finished my first Plex plugin...

 

  • Navigation through my menu works fine.
  • Listing all video objects works fine, too.
  • Accessing the video shows me the correct metadata too.
  • Streaming the videos with browsers like firefox, chrome works.

 

But:

Using the iOS App works fine, except playing the videos. The player appears but does not show the video...

My first thought was that the device cannot play the url directly. After calling the url directly on the device the iOS player was started and the video is displayed...

 

I don't have a clue why the video cannot be played within the Plex iOS app. Perhaps someone has a hint for me whats going wrong and/or how I can debug this?

 

I've found "extended logging" settings within the app. Where do all these logs go?

 

Hope I can finish my plugin successfully...

Greets

Sascha

I would start by checking the Plex Media Server.log for errors related to playback. It almost sounds like the iOS player is waiting for PMS to transcode the stream. Can you paste or link the playback-related portion of your plugin code?

Of course..

Main handler:

def MainMenu():
  Plugin.AddViewGroup('InfoList', viewMode='InfoList', mediaType='items')

oc = ObjectContainer(title1=TEXT_TITLE, view_group = ‘InfoList’)

token = getToken(“test”)

Only process if we got a valid token.

if token != “”:
result = getChannels(token)
if debug == True: Log("Token to use for api calls: " + token)

for channel in result['channels']['elements']:
  oc.add(createTVChannelObject(channel))

oc.add(PrefsObject(title=TEXT_PREFERENCES, thumb=R(ICON_SETTINGS)))

if len(oc) < 1:
failoc = ObjectContainer(header=“Fehler!”, message=“API Zugriff nicht möglich.”)
failoc.add(PrefsObject(title=TEXT_PREFERENCES, thumb=R(ICON_SETTINGS)))
return failoc

return oc

Videoobjects are created the following way:

def createTVChannelObject(channel, container = False):
  uniqid = channel['vodkatvChannelId']
  name = channel['name']
  thumb = channel['logo']
  url = channel['urls'][1].replace('http-interoud','http')
  summary = ""
  epg_title = ""
  epg_description = ""
  epg_start = ""
  epg_end = ""
  epg_length = 0

if channel[‘currentNext’].has_key(‘current’):
if channel[‘currentNext’][‘current’].has_key(‘title’):
epg_title = channel[‘currentNext’][‘current’][‘title’]
epg_description = channel[‘currentNext’][‘current’][‘description’]
epg_start = datetime.datetime.fromtimestamp(channel[‘currentNext’][‘current’][‘start’]/1000).strftime(’%H:%M’)
epg_end = datetime.datetime.fromtimestamp(channel[‘currentNext’][‘current’][‘finish’]/1000).strftime(’%H:%M’)
epg_length = int(channel[‘currentNext’][‘current’][‘length’])

if container == False:
summary = epg_title
else:
summary = epg_start + " - " + epg_end + "

" + epg_title +"

" + epg_description

vco = VideoClipObject(
key = Callback(createTVChannelObject, channel = channel, container = True),
rating_key = uniqid,
title = name,
summary = summary,
duration = epg_length,
thumb = thumb,
items = [
MediaObject(
video_codec = VideoCodec.H264,
audio_codec = AudioCodec.AAC,
audio_channels = 2,
optimized_for_streaming = True,
parts = [PartObject(key = url)]
)
]
)

if container:
return ObjectContainer(objects = [vco])
else:
return vco
return vco

Hope this helps...

I suspect the issue is that the channel variable is not being serialized and passed properly between the initial call to createTVChannelObject() and the subsequent callback to the same function. Try adding a @route declaration, something like this:

@route(CHANNEL_PREFIX + '/createchannel', channel=dict)
def createTVChannelObject(channel, container = False):
...

Failing that, there are a few other possibilities that might make a difference; you could set the container type, or if the stream is HLS use the HTTPLiveStreamURL() to set the container and codec info for you like so:

MediaObject(
       parts = [PartObject(key = HTTPLiveStreamURL(url))]
     )

If using the HTTPLiveStreamURL() function or adding the proper container type (if not HLS) doesn't work, you might try returning the media url via a Callback. It shouldn't be necessary but is more commonly used than not in most channels and URL Services.

MediaObject(
       parts = [PartObject(key = Callback(PlayVideo, url=url))]
     )

@indirect
def PlayVideo(url):
return IndirectResponse(VideoClipObject, key=url)

Thanks...I cannot get the point of "route"...

In your example you use "CHANNEL_PREFIX" and "channel=dict". But cannot transport it to my needs and what this "route" command does. Examining the documentation of route didn't help me understand it.

Perhaps you can show me the right direction...

I've added the route:

@route('/video/tv/createchannel', channel=dict)

Within the plugin log I can see really long get requests...

2013-12-02 09:20:42,338 (7f03625e4700) :  DEBUG (networking:172) - Requesting 'http://127.0.0.1:32400/:/plugins/com.plexapp.system/messaging/function/X1N0b3JlU2VydmljZTpJc0NoYW5uZWxCcm9rZW4_/Y2VyZWFsMQoxCmxpc3QKMApyMAo_/Y2VyZWFsMQoxCmRpY3QKMQpzMjUKY29tLnBsZXhhcHAucGx1Z2lucy5ld2V0dnMxMAppZGVudGlmaWVycjAK'
2013-12-02 09:20:42,346 (7f03625e4700) :  WARNING (objectkit:199) - Media part has no streams - attempting to synthesize
2013-12-02 09:20:42,389 (7f03625e4700) :  DEBUG (runtime:106) - Sending packed state data (106 bytes)
2013-12-02 09:20:42,390 (7f03625e4700) :  DEBUG (runtime:914) - Response: [200] MediaContainer, 216680 bytes
2013-12-02 09:20:55,447 (7f03625e4700) :  DEBUG (runtime:717) - Handling request GET /video/tv/createchannel?container=True&channel=%7B%22vodkatvChannelId%22%3A+%22int_ticket_daserste%22%2C+%22currentNext%22%3A+%7B%22current%22%3A+%7B%22finish%22%3A+1385974500000%2C+%22description%22%3A+%22Johanna+schw%5Cu00e4rmt+nicht+nur+f%5Cu00fcr+die+Weine+des+Burgenlandes%2C+sondern+besonders+f%5Cu00fcr+den+Winzer+Ferdi+Hofleitner.+Das+bemerkt+Thomas+und+vermutet%2C+dass+Johanna+einen+Weinladen+er%5Cu00f6ffnen+m%5Cu00f6chte%2C+doch+weit+gefehlt+...+S%5Cu00f6nke+hofft%2C+dass+Brian+ihm+nicht+die+Polizei+auf+den+Hals+hetzt%2C+nachdem+er+bei+Malakron+eingedrungen+ist.+Die+dort+entwendeten+Tabellen+geben+Jule+wenig+Aufschluss%2C+so+bittet+sie+Thomas%2C+sich+die+Unterlagen+anzusehen.+W%5Cu00e4hrend+in+Brian+leise+Zweifel+an+Ulis+Version+von+Jules+Suchtr%5Cu00fcckfall+wachsen.+Als+Rieke+sich+den+Fu%5Cu00df+verstaucht%2C+erweist+sich+Mick+als+wahrer+Gentleman%3A+Er+%5Cu00fcberl%5Cu00e4sst+den+Frauen+den+Wagen%2C+er+hingegen+eiert+mit+beiden+Fahrr%5Cu00e4dern+zur%5Cu00fcck.+Tine+hofft%2C+durch+das+Zusammenleben+mit+Lotte+ihrer+Tochter+wieder+n%5Cu00e4herzukommen. [...]

But streaming does not start...

The app still gives me "An unknown problem occurred".

Is there any chance to see the APP logfile?

Note: After changing to HTTPLiveStreamURL it behaviour has changed. Before nothing happend, now the error is displayed. It's a HLS stream delivered by Wowza Media Servers...

Strange thing... I came back to home and tried to stream with the ios app again. And it works! But leaving my local network I cannot stream anymore. The unknown error is displayed...

The server where the stream comes from is hosted within the internet, not my local home. So I don't understand why I can stream at home but not when I'm somewhere else.

At home:

- Streaming via iOS app works

- Streaming via browser works

Mobile:

- Streaming via iOS app fails

- Streaming via browser works

Any suggestions for this really strange behaviour? :-)

Greets

Sascha

Ah, that sounds like it might be an issue where the HLS server is rejecting the playback request (when mobile) because the IP of the player making the request is different than the IP (of the server) making the initial requests. Possibly due to IP tracking, or possibly due to cookie caching.

There is a "post_url" parameter in the framework for handling IP-specific requests. The URL Service for the ABC channel is a good example of it's use. See here.

The other possibility is that the HLS Server requires some cookie(s) be returned with the playback request. The NBC URL service is a good example of how that can be implemented. See here.

The @route decorator for plugin functions is intended to provide a more RESTful interface and allowing casting variable in the appropriate type to avoid situations where they might be de-serialized incorrectly.

Thanks, I'll try my luck with the post_url argument. First of all I have to find out how to use it...

Seems that the server may not fetch the token and do the login process... The client (browser, iOS app) should do this.

So every call to the api + streaming access should be done by the "client".

Is there a way to let the clients (browsers, ios apps, ..) do this and give the result back to the plex server that it can build the listings etc..?

That is essentially the concept behind the post_url parameter but on a smaller scale. When passing an arg to and ObjectContainer with the post_url parameter, the client fetches the content of the URL and passes it back to PMS for manipulation. It might be possible to use the post_url params in several places in your code to allow for clients fetching tokens etc. but I’m not aware of any examples where this has been done. You’re in somewhat uncharted territory in this regard.

Thanks, I'll investigate this and come back to you with the result :-)

Can you write an example procedure/function which shows how the "post_url" works? I cannot figure out to make the client do post requests to the api with given username/password combinations and return the result to pms.

I know this is an undiscovered "country" within pms but perhaps with your deep understanding you'll have more success to write such a function.

Ok. I'll see what I can come up with. Before I do, can you clarify the requirements for you situation?

  • Do navigation elements for you plugin (return lists of videos, etc.) require being logged in? or is it possible to navigate without a login token and only playback requires a login?
  • as far as we know, the token is IP specific rather than device specific?

I'll try to describe the situation:

  • There's a JSON api from which I need to get a token. To get the token I need to suply a username/password.
  • After getting the token I can use it the fetch channellist, epg data, favorites etc. from the api. These data is delivered by json replies.
  • If I go through the channelist reply I can find the URLs to the streams for the tv-channels which are delivered by hls streams. The token is added to the stream url.
  • After a time of 5-15 minutes I have to renew the token.

As you can see there are only some contacts to an api which has to be done by the client. Finally the stream of the tv channels will be delivered to the client.

Greets

Sascha

Would it be problematic to renew the token early? If not, I think the best implementation is likely to have the channel code login and use that token for building directories, navigation, etc. Then a separate login will occur (using a post_url) when the client requests playback. I’ll try to put together an example if it sounds like that flow should work.

The token can be renewed at every time.

Based on your answers and pasted code above, I would recommend something like this:

def createTVChannelObject(channel, container = False):
  uniqid = channel['vodkatvChannelId']
  name = channel['name']
  thumb = channel['logo']
  url = channel['urls'][1].replace('http-interoud','http')
  summary = ""
  epg_title = ""
  epg_description = ""
  epg_start = ""
  epg_end = ""
  epg_length = 0

if channel[‘currentNext’].has_key(‘current’):
if channel[‘currentNext’][‘current’].has_key(‘title’):
epg_title = channel[‘currentNext’][‘current’][‘title’]
epg_description = channel[‘currentNext’][‘current’][‘description’]
epg_start = datetime.datetime.fromtimestamp(channel[‘currentNext’][‘current’][‘start’]/1000).strftime(’%H:%M’)
epg_end = datetime.datetime.fromtimestamp(channel[‘currentNext’][‘current’][‘finish’]/1000).strftime(’%H:%M’)
epg_length = int(channel[‘currentNext’][‘current’][‘length’])

if container == False:
summary = epg_title
else:
summary = epg_start + " - " + epg_end + "

" + epg_title +"

" + epg_description

vco = VideoClipObject(
key = Callback(createTVChannelObject, channel = channel, container = True),
rating_key = uniqid,
title = name,
summary = summary,
duration = epg_length,
thumb = thumb,
items = [
MediaObject(
parts = [PartObject(key = HTTPLiveStreamURL(Callback(PlayVideo, url=url, post_url=LOGIN_URL)))]
)
]
)

if container:
return ObjectContainer(objects = [vco])
else:
return vco
return vco

@indirect
def PlayVideo(url): # note post_url is not included in the list of key_word args
‘’’ use LOGIN_URL to execute login and get necessary token ‘’’
final_url = url + token # or however it needs to work
return IndirectResponse(VideoClipObject, key=HTTPLiveStreamURL(final_url))

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.