Request auth hash when user presses play

To access some Nimble streams, my pluging needs to:

  1. authenticate a user
  2. hit a php file, get an authentication hash,
  3. amend that hash to the media URL

This all needs to happen when the user presses play as the hash is only valid for a short period of time.

Is this doable, and if so how would I go about doing that?

The current media function I have is:

CHANNEL_OBJECT = VideoClipObject( key = Callback( CreateChannelEpisodeObject, TITLE = TITLE, URL = URL, SUMMARY = SUMMARY, THUMB = THUMB, INCLUDE_CONTAINER = True ), rating_key = URL, title = TITLE, summary = SUMMARY, thumb = R(THUMB), items = [ MediaObject( video_resolution = RESOLUTION, width = WIDTH, height = HEIGHT, audio_channels = 2, optimized_for_streaming = True, video_frame_rate = 25, parts = [ PartObject( key = HTTPLiveStreamURL( url = URL ) ) ] ) ] )

So I’m unsure where to place the calls to get the extra hash.

So I’ve added a callback to :

Key = HTTPLiveStreamURL( url = GetAuthenticatedURL(URL) )

Which seems to work in terms of building the correct URL and passing it back, but I’m unsure if this will be asked for everytime the user presses play.

Any thoughts appreciated.

If I understand it correctly, GetAuthenticatedURL is not a callback, you’re just calling a function and it’s executed every time the VideoClipObject is built. Which is probably every time directory content is displayed. In order to create a callback, use something like this:

    key=Callback(GetAuthenticatedURL, url=URL)

This way GetAuthenticatedURL function will be called only when the Part URL is being resolved, ie when playing it.

Many thanks @czukowski – will give that a go.

Okay so I now have this in the code

key = HTTPLiveStreamURL(
  url = Callback(
    CreateAuthenticatedURL,
    URL = URL
  )
)

Will this be the same as your example @czukowski?

@papalozarou to be honest, I never used HTTPLiveStreamURL function myself. If it works as intended, then I think you’re done.

To find out, I suggest adding some temporary logging to your CreateAuthenticatedURL and see if that line shows up while you’re browsing the channel, but not playing the video:

def CreateAuthenticatedURL(URL):
    Log("I've been called!")
    ...

Note, that if your code that generates your Media Object resides in the URL Services, then you need to look into com.plexapp.system.log file and not the one that belongs to your channel.

If your testing line still appears in the logs, try changing the code by assigning Callback to the part’s key:

    PartObject(key=Callback(CreateAuthenticatedURL, url=URL))
    ...

def CreateAuthenticatedURL(URL):
    Log("I've been called!")
    url_with_hash = ...   # this is where you do your HTTP requests to get the authenticated media URL
    return HTTPLiveStreamURL(url=url_with_hash)  # or maybe just `return url_with_hash`?

If you don’t already, try looking at the XML generated by PMS in the browser, it may help a lot as you’ll see the exact URL generated for the Part Object.

@czukowski said:
@papalozarou to be honest, I never used HTTPLiveStreamURL function myself. If it works as intended, then I think you’re done.

HTTPLiveStreamURL is not really special, it’s just a shortcut function to set some media object values automatically.

Thanks for the replies again both of you. I’m just picking this back up again after a couple of weeks out, so will try the above suggestions.

I tried every the original method:

key = HTTPLiveStreamURL( url = Callback( CreateAuthenticatedURL, URL = URL ) )

Which gives an error:

Manifest is not valid M3U8 file

And appears to be trying to play CreateAuthenticatedURL.m3u8 and in the browser console it says:

canPlay: true canDirectPlay: true canDirectStreamVideo: true canDirectStreamAudio: true videoResolution: 720

I then tried the above suggestions:

key = Callback( CreateAuthenticatedURL, URL = URL )

With CreateAuthenticatedURL returning either a plain URL or HTTPLiveStreamURL, which gives an error:

There was a problem playing this item

And in the browser console it says:

canPlay: true canDirectPlay: false canDirectStreamVideo: false canDirectStreamAudio: false

Whilst in the PMS log it says:

Direct Play is disabled container is unavailable for analysis no direct play video profile exists for http// no direct play video profile exists for http/// codec is unavailable for analysis codec is unavailable for analysis no remuxable profile found, so video stream will be transcoded computed resolution bounding box of 1280x738. codec is unavailable for analysis

All of which doesn’t appear when I try the original method (Referenced at the start of this post).

In all cases the logged URL from the CreateAuthenticatedURL plays fine in VLC.

The only way I have currently got this to work is if I generate the authenticated URLs when the video list menu is generated and pass that URL in directly to the MediaObject.

So I guess the question is does the menu get generated every time or is it cached? The time limit on the authentication is 10 minutes.

I think it’s up to client app whether to cache menu or not. Plex Server will generate it every time it’s called, unless you implement some caching specifically. Anyway, your approach may work, but there’s still a possibility to leave the directory open in the client for 10 minutes or more, then playing those items may not work due to expired authentication hash.

I used to develop a channel for a shoutcast based audio stream that also uses an m3u playlist, where I used Redirect function instead of HTTPLiveStreamURL, so you could also try it to see it it makes any difference in your case:

# Redirect to url. Must have extension parameter so that Plex Home Theater can play it.
# PHT seems to rely on file extension in the URL to find the correct decoder.
@route(PREFIX+"/play/{base_url}.m3u")
def PlayAudio(base_url):
    url_with_hash = ...
    Log('Playing \'%s\'' % url_with_hash)
    return Redirect(url_with_hash)

A callback to PlayAudio function is passed to PartObject.

@papalozarou said:

So I guess the question is does the menu get generated every time or is it cached? The time limit on the authentication is 10 minutes.

The cache depends on your HTTP Request and any cache values in your code.
For example an overall default cache value like:
def Start(): HTTP.CacheTime = CACHE_1HOUR
Or a cache value for an individual HTTP request like:
def MyFunction() content = HTTP.Request(url, cacheTime=CACHE_1DAY).content

If you are instead referring to using the Plex Web app for channel development, it is never a good choice because it will not update any changes you make to a channel for 24 hours unless you clear the browser’s cache. Here is a good discussion about channel development that discusses the issues of using Plex Web for channel development - forums.plex.tv/discussion/208369/whats-your-channel-development-and-debugging-workflow-and-why-is-mine-so-painful#latest

If the issue is with the creating of the actual media object and you do not have a separate URL service, make sure you have created a separate function to create those media objects that loops through twice. There are several examples of channels without a URL service that have a CreateVideoClipObject function out there, like HGTV - github.com/plexinc-plugins/HGTV.bundle/blob/master/Contents/Code/init.py#L176

You can also look at the XML documents for a channel including all of its menus and even include the client requesting it by adding “X-Plex-Platform=” to your request. So for example, the XML for the Primetime directory of the CBS channel for the Plex player for Roku would be http://localhost:32400/video/cbs/shows?category=primetime&cat_title=Primetime&X-Plex-Platform=Roku&X-Plex-Token=.

Thanks to both @shopgirl284 and again @czukowski for your replies.

@shopgirl284 – just so I am clear, you mention HTTP requests, but I’m trying to see if I the menu of available media objects that Plex generates is cached, or it’s generated every time a user hits that page. Or does that still depend on the original HTTP request?

@czukowski – I tried your @indirect suggestion:

@indirect def PlayVideo(URL): AUTHENTICATED_URL = CreateAuthenticatedURL(URL) Log("Playing " + AUTHENTICATED_URL) return IndirectResponse(VideoClipObject,key=AUTHENTICATED_URL)

If I do this:

items = [ MediaObject( video_resolution = RESOLUTION, width = WIDTH, height = HEIGHT, audio_channels = 2, optimized_for_streaming = True, parts = [ PartObject( key = Callback( PlayVideo, URL = URL ) ) ] ) ]

The clip will play in PHT and iOS but not web or Android.

If I add audio_codec = AudioCodec.AAC, video_codec = VideoCodec.H264 and container = Container.MP4 values to the MediaObject , the clip will play in PHT and web, but not Android or iOS.

If I try audio_codec = "AAC", video_codec = "H264" and container = "MP4" it will play in PHT, web and iOS (though iOS will constantly display a spinner), but not Android. If I remove container = "MP4" it will only play in PHT and web.

So basically Android seems to be the issue here and I seem to remember having the same problem a while ago with a previous interation of this plugin, however I’m not sure how that particular thing fixed itself.

The only difference I can see with it not playing on Android is that using the ‘@indirect’ method the following shows in the console:

canPlay: true canDirectPlay: false canDirectStreamVideo: false canDirectStreamAudio: false

But parsing the URL when the menu is generated, rather than when the play button is pressed, yields the following:

canPlay: true canDirectPlay: true canDirectStreamVideo: true canDirectStreamAudio: true

Is there a way to force those settings to true? Or does Plex server basically figure that out itself?

So I’ve fixed it with an if else statement that checks the value of Client.Platform and provides a MediaObject specifically for Android and then a different one for other clients. It seems to work.

@papalozarou you could open the resulting XML in your browser and compare the difference between the two approaches. That might give you an idea how to build the output that will play on Android and generate the authenticated URL at the play start time instead of menu.

I don’t know if it helps, but here’s an output structure from another channel that doesn’t play video streams, but just files. By “structure” I mean simplified YAML-ish syntax and stripped of non-essential attributes like ‘title’, ‘summary’ and so on.

Directory (menu):

ObjectContainer:
    - MovieObject:  # oc.add(MovieObject(...))
          key: Callback(PlayVideo, url=movie_url)
          rating_key: movie_url
          items: CreateMediaObjectsForURL(Callback(PlayVideo, url=movie_url))

CreateMediaObjectsForURL(callback) (not from URL Service, just similarly named function in Code/__init__.py, could just as well may be, but then Plex matches menu content to URL Services a bit differently and the menu structure should be adjusted as well; also based on the function name it should actually take URL, not callback, but I found it convenient since it just passes the callback down to the PartObject):

- MediaObject:  # function returns array
      parts:
          - PartObject(key=callback)

Note: the function returns array, may contain more than one MediaObject.

PlayVideo:

def PlayVideo(movie_url):
    # actual call to a function `CreateVideoObjectContainer`
    return CreateVideoObjectContainer(movie_url)

Note: creating video object container should be in its own function because it’ll include a callback to itself.

CreateVideoObjectContainer(movie_url)

ObjectContainer:
    no_cache: true
    objects:  # `objects` attribute is array containing a `VideoClipObject`
        - VideoClipObject:
          key: Callback(CreateVideoObjectContainer, movie_url=movie_url)  # callback to self
          rating_key: movie_url
          items:
              - MediaObject:
                    parts:
                        - PartObject:
                              key: key=Callback(RedirectToActualVideoUrl, movie_url=movie_url)

RedirectToActualVideoUrl:

def RedirectToActualVideoUrl(movie_url):
    movie = WebsiteMoviePage(movie_url)
    video_url = movie.video_url
    Log('Redirecting to video URL: %s' % video_url)
    return Redirect(video_url)

May be overcomplicated, in part due to the fact I didn’t use URL Services here, but seems to be working for me.

thanks @czukowski – I think I am doing a lot of that already, or have tried it, but I only scanned your post just now so will take a look in greater detail when I get some more time to work on the plugin again. Again I appreciate your efforts to help me.