Welcome to our forums! Please take a few moments to read through our Community Guidelines (also conveniently linked in the header at the top of each page). There, you'll find guidelines on conduct, tips on getting the help you may be searching for, and more!

Best practice regarding passing/storing of m3u8 urls

kvoldenkvolden Members Posts: 7 ✭✭

I have a URL service, which has a MetadataObjectForURL function. This naturally uses the passed url to fetch metadata for the video in question, and one of these datafields that are fetched is the m3u8 url. I tried attaching a mediaobject with the metadataobject before returning it, like in the following snippet:

   meta_object.add(
        MediaObject(
            video_resolution = 720,
            optimized_for_streaming = True,
            audio_channels = 2,
            parts = [
                PartObject(
                    key = HTTPLiveStreamURL(media_json['mediaUrl'])
                )
            ]
        )
    )

Unfortunately, this doesn't consistently work. It works with (perhaps incidentally) the live streams, but for the other streams – with the exact same container and codecs – the plugin halts/crashes with complaints in the log about missing associated MediaObjectsForURL functions.

Now, I've seen it said several places that MediaObjectsForURL functions should execute quickly and not perform any HTTP requests because of the delay in playing that the user might experience. But to get the m3u8 url in this function, I would need to do another lookup to the same url as in MetadataObjectForURL. I could pass it as an argument to a PlayVideo and add that as a callback, but then PlayVideo would have to do the lookup, which – if I understand the flow correctly – would cause the same delay for the user.

So the questions are: Why is it not consistently working the way I tried above? And if I have to make a MediaObjectsForURL function, how do I avoid doing several lookups to the same url, causing unnecessary delays? I'm sure there must be a best practice to this, and a way to avoid repeatedly doing the same request.

Best Answer

Answers

  • kvoldenkvolden Members Posts: 7 ✭✭

    First of all, thanks a million for helping. :)

    @czukowski said:
    @kvolden I can't comment on why it's working some times and not the other times, you should consult the log files for it. See if the web page URLs are somehow significantly different between working and non-working streams. If you see any error messages with stack traces, you may be in luck, at least you'll have something to go on :)

    There's no difference in the url except for the program id. No stack traces. Only this:

    2017-05-15 21:20:34,404 (7ff3a77fe700) :  DEBUG (services:616) - Found a service matching 'http://tvapi.nrk.no/v1/programs/kmte41003109' - NRK (com.plexapp.plugins.nrk)
    2017-05-15 21:20:34,412 (7ff3a77fe700) :  DEBUG (services:616) - Found a service matching 'http://tvapi.nrk.no/v1/programs/kmte41003109' - NRK (com.plexapp.plugins.nrk)
    2017-05-15 21:20:34,416 (7ff3a77fe700) :  DEBUG (services:616) - Found a service matching 'http://tvapi.nrk.no/v1/programs/kmte41003109' - NRK (com.plexapp.plugins.nrk)
    2017-05-15 21:20:34,418 (7ff3a77fe700) :  CRITICAL (sandbox:298) - Function named 'MediaObjectsForURL' couldn't be found in the current environment
    

    Guess I'll switch to using a MediaObjectsForURL function for now, even though it will cost me one more HTTP request per item. Unless you know of any other brilliant approach?

    @czukowski said:
    About the unnecessary lookups, there is a difference between calling PlayVideo function and using it in a callback in MediaObjectsForURL. In the second case, a lookup is made only when the item is actually played:

    I'm aware of the technical difference between direct calling and passing as callback, but I thought that both PlayVideo and MediaObjectsForURL were called when the item was played. I take it that MediaObjectsForURL is called for each item when generating the list of items, and it's the accumulated delay of one call per item shown that people are talking about? If so, why are synchronous requests in MediaObjectsForURL more of a concern than synchronous requests in MetadataObjectForURL, which is also called on item list generation?

  • czukowskiczukowski Members Posts: 124 ✭✭
    edited May 16

    If you use a callback, only PlayVideo will be called on play.

    The reason is in the MediaContainer XML that is generated by Plex for the clients. When you add EpisodeObjects (or whatever) with urls, it generates an XML with a structure typically like this:

    MediaContainer:
        -
            Video(url='...', key='/system/services/url/lookup?url=...'):
                Media:
                    Part(key='/:/plugins/_plugin_key_/serviceFunction/url/_plugin_key_/_url_service_key_/PlayVideo?args=_encoded_arguments_'):
                        - Stream(streamType=1)
                        - Stream(streamType=2)
        -
            Video(url='...'):
                ...
    

    You can see the data from both MetadataObjectForURL and MediaObjectsForURL (that means it has not been called asynchronously as I said earlier). If you do load the webpage in both of those functions, then yes, it'll be parsed twice, although HTTP object in Plex supports request cache for a period of time, you can make it actually do a HTTP request only once. In many examples I've seen, the MediaObjectsForURL actually does a guesswork about the containers, codecs, etc, probably because it's often the same across all or most media on a given website. It could also be that some of the wrongly stated info (if the guess was wrong) will not make the media unplayable, although that may be different for different Plex clients.

    The Part object's key argument is a direct callback to your PlayVideo function, so the client doesn't have to call media or metadata functions again.

  • czukowskiczukowski Members Posts: 124 ✭✭
    edited May 16

    Now let's see what the response is if we load the URL from the Video key attribute:

    MediaContainer:
        Video(url='...', key='/system/services/url/lookup?url=...'):   # <-- it actually has a link back to itself
            Media:
                Part(key='/:/plugins/com.plexapp.system/serviceFunction/url/_plugin_key_/_url_service_key_/PlayVideo?args=_encoded_arguments_'):
                    - Stream(streamType=1)
                    - Stream(streamType=2)
    

    I guess this represents a 'pre-play' screen.

    The Part key is a little bit different this time, I think it's effectively just an alias for the same function. The encoded arguments are the same as above. It may also mean that whatever occurs here will be logged to com.plexapp.system.log instead of your plugin's log file.

    An important part here is that a channel doesn't have to parse the actual media URL up to the point where it's supposed to start playing.

  • kvoldenkvolden Members Posts: 7 ✭✭

    @czukowski said:
    You can see the data from both MetadataObjectForURL and MediaObjectsForURL (that means it has not been called asynchronously as I said earlier).

    Thanks for taking the time, but I still don't understand the reasoning behind the advice to not do requests in MediaObjectsForURL specifically, while it is okay in MetadataObjectForURL, when they are seemingly run successively and synchronously. I've seen this advice several times, like for instance (https://support.plex.tv/hc/en-us/articles/201382123-The-Power-of-the-URL-Service "here"). Now, I don't need to do that as I'll use the PlayVideo callback, but it still makes me feel like I've misunderstood something.

  • czukowskiczukowski Members Posts: 124 ✭✭

    To be honest, I don't know that either. Although you're welcome to dive into the Plex Framework source code and try and find out :)

  • czukowskiczukowski Members Posts: 124 ✭✭

    Here's one curious note from the MetadataObjectForURL docs, where the "function defined below" is MediaObjectsForURL:

    Note: Although the object’s media items can be created here, this isn’t necessary. If no
    items are provided, they will be populated using the function defined below.

    Seems to be true, because while using that and the following MediaObjectsForURL implementation the channel still works:

    def MediaObjectsForURL(url):
        # Not implemented
        raise Ex.MediaNotAvailable
    
Sign In or Register to comment.