Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Try to fully support key rotation AND maxSessionCacheSize on devices with very limited key slots #1511

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from

Commits on Aug 29, 2024

  1. [WIP] Try to fully support key rotation AND maxSessionCacheSize on de…

    …vices with very limited key slots
    
    Since monday, I'm trying to fix the theoretical issue described here,
    but it proved very hard to actually implement.
    
    As such, I chose to open its Pull Request despite it still being
    non-functional, to expose (to other developers and also to myself), what
    is tried and what wall is encountered here.
    
    We may in the end try different solutions.
    
    The issue
    =========
    
    Preamble: conditions
    --------------------
    
    This work is about a specific and for now never seen issue that has
    chance to pop up under the following conditions:
    
      - The current content make use of per-Period key rotation (NOTE: this
        is also possible without key rotation, but this is made much more
        probable by it).
    
      - The device on which we we play that content has a very limited number
        of key slots available simultaneously for decryption  (sometimes
        **VERY** limited, e.g. `6`, so we can at most rely on 6 keys
        simultaneously.
    
        We for now know of two different set-top boxes with that limitation.
    
    Problem with the `maxSessionCacheSize` option
    ---------------------------------------------
    
    Theoretically, an application can rely there on the
    `keySystems[].maxSessionCacheSize` option to set a maximum number of
    `MediaKeySession` we may keep at at the same time.
    
    _Note that we prefer here to rely on a number of `MediaKeySession` and
    not of keys, because the RxPlayer is not able to predict how many keys
    it will find inside a license (NOTE: to simplify, let's just say that
    1 `MediaKeySession` == 1 license here, as it's very predominantly the
    case) nor is it able to not communicate some keys that are found in a
    given license._
    _Yet an application generally has a rough idea of how many keys it's
    going to find in a license a most, and can set up its
    `maxSessionCacheSize` accordingly (e.g. if there's max `10` key slots
    available on the device and `5` keys per license maximum, it could just
    communicate to us a `maxSessionCacheSize` of `2`)._
    
    So problem solved right? WRONG!
    
    The RxPlayer, when exploiting the `maxSessionCacheSize` property, will
    when that limit is reached just close the `MediaKeySession` it has least
    recently seen the need for (basically, when the RxPlayer loads segment
    for a new Period/track/Representation, it asks our decryption logic to
    make sure it has the right key, this is how our decryption logic know
    that a `MediaKeySession` or more precizely a key it can make use of,
    has been needed recently.
    
    Example scenario
    ----------------
    
    So let's just imagine a scenario:
    
      1. we're currently loading a Period A with encrypted content and
         buffering a future Period B with content encrypted with a different
         key.
    
      2. we're asking the decryption logic to make sure the key is loaded
    
      3. The decryption logic sees that it doesn't have the key yet, and
         thus has to create a new `MediaKeySession`.
    
         Yet, it sees that it cannot create a new `MediaKeySession` for that
         new key without closing an old one to respect the
         `keySystems[].maxSessionCacheSize` option, and it turns
         out one relied on to play Period A was the least recently needed for
         some reason.
    
      4. The decryption logic closes a `MediaKeySession` for Period `A` that
         was currently relied on.
    
      5. ??? I don't know what happens, the `MediaKeySession` closure may
         fail in which case we could be left with too many key slots used on
         the device and some random error, or content playback may just fail
         directly.
    
         In any case, I wouldn't bet on something good happening.
    
    Other types of scenarios are possible, e.g. we could be closing a
    `MediaKeySession` needed in the future and not think to re-create it
    when playing that future Period, potentially leading to a future
    infinite rebuffering.
    
    Solution I'm proposing here
    ===========================
    
    The solution I tried to implement tries the following logic:
    
      - When closing a `MediaKeySession` linked to the current content, it
        will stop relying on "least-recently used" principles (this
        algorithm will still be relied on for "cached" `MediaKeySession` -
        e.g. when switching between multiple TV channels).
    
        The idea here is more aligned with usual buffer-linked eviction
        algorithms:
    
      - We'll begin to close `MediaKeySession` linked only to
        already-played `Period`, in theory beginning by those further in
        the past.
    
        In reality let's just close all of them.
    
      - if it's not sufficient, we'll close `MediaKeySession` linked to
        `Period` distant in the future (like further in the future first).
    
      - if it's not sufficient, e.g. if even just for the current Period
        there's too many `MediaKeySession`, I'm not sure of what to do for
        now (probably do nothing and let things break)
    
    This only take into consideration the concept of `Period`, even if
    theoretically a `MediaKeySession` could only be linked to a particular
    track of `Representation`.
    
    This is only because - when implementing the solution - my brain already
    hurt thinking about the very simple concept of per-Period licenses, I
    didn't want to go to other aspects, we'll see those details later (even
    if they are also very possible - the solution I write here also work for
    them, even if we could also have an even better one taking into
    consideration e.g. the current tracks).
    
    How I'm implementing this
    =========================
    
    On the `ContentDecryptor`-side
    ------------------------------
    
    This feature has been a little hard to actually implement.
    
    First, as we now have a difference in our `MediaKeySession`-closing
    algorithm depending on if the `MediaKeySession` is linked to the current
    content or not, I chose in our `ContentDecryptor` module that:
    
      1. `MediaKeySession` that are not linked to the current content keep
         being closed as before: least recently needed first.
    
      2. `MediaKeySession` that are linked to the current content are never
         directly closed by the `ContentDecryptor`.
    
         Instead, the `ContentDecryptor` module basically signals, when only
         left with `MediaKeySession` for the current content yet going over
         the `maxSessionCacheSize` limit, a `tooMuchSessions` event and lock
         its queue of protection data (it stops basically creating new
         `MediaKeySession` until older sessions are closed).
    
    The `ContentDecryptor` also exposes a new method, `freeKeyIds`. You
    communicate to it the key id you don't need anymore, then the
    `ContentDecryptor` will see if can consequently close `MediaKeySession`,
    then check if the `maxSessionCacheSize` is respected, restart its queue
    if it is, and so on.
    
    On the `ContentInitializer`-side
    --------------------------------
    
    The `ContentInitializer` module then reception `tooMuchSessions` events
    coming from the `ContentDecryptor`, and rely on our newly-defined
    algorithm to know which `MediaKeySession` it can let go.
    
    Though this is where for now I'm lost due to edge cases:
    
      - When closing `MediaKeySession` linked to future `Period`, the
        `Stream` modules loading their linked content might be in the
        process of running, and they won't know for now that the
        `MediaKeySession` has been closed and thus not think about re-asking
        for the decryption key.
    
        There's many potential solutions here (relying on the concept of
        `lockedStream`, reloading, actually communicating to those streams
        that for now a `MediaKeySession` is closed, re-asking for the right
        key once the content is playing inside the `ContentInitializer`), yet
        any of them are very hard to get right and all of them have
        inconvenients.
    
      - There's still the issue of what should happen if we're left only
        with the current Period. I already lost many neurons on the simpler
        first simpler future-Period case, and in consequence had none to
        spare for this more complex possibility.
    
      - This is on another level, but what if while we were running an
        algorithm, some random `MediaKeySession` is closed by the browser (or
        some RxPlayer/application logic running in concurrence)?
    
        We may theoretically go below the `maxSessionCacheSize` limit at any
        time, due to some outside behavior, and in the end not actually need
        to continue running the algorithm.
    
        For now, even if that one is possible, I chose that having perfect
        code that would restart processing init data by itself due to that
        type of event too difficult to actually implement.
    
        So we'll probably should not show broken behavior if that
        actually happens, but I wasn't explicitely handling it here.
    peaBerberian committed Aug 29, 2024
    Configuration menu
    Copy the full SHA
    19f6f54 View commit details
    Browse the repository at this point in the history