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

[Matrix] License Renewal for Youtube movies #589

Merged
merged 3 commits into from
May 17, 2021

Conversation

MisterD81
Copy link
Contributor

At least purchased movies in Youtube addon stopped working. Always after 5 minutes we can see in the logs that there is no decryption key. Surprisingly I was only able to get this license behavior only on Windows and Linux ARM. On Linux x64 the license renewal was not required.
See the corresponding bug in YT addon: anxdpanic/plugin.video.youtube#35

I figured out with EME logger of Chrome that the CDM is sending periodically a session message to renew the license.
IAS is not doing that at the moment which results in the behavior that playback always stops after 5 minutes. I found in the code the commented-out parts for the license renewal and activated them. The additional adjustments were required to ensure that the thread is stopped correctly. Otherwise Kodi crashes when the playback is finished or is stopped.

We can argue about the sleep duration, but I could not observe a high CPU increase or high delay when playback stopped with the 100ms.

Since I'm still on Leia this patch is only created based on the Leia PR (#588)

Ensure that Timer stops before CDM Adapter stops
@glennguy
Copy link
Contributor

@MisterD81 Thanks for the PR, sorry just getting to it now. Looks fine to me and doesn't crash but @phunkyfish could you have a once over whenever suits? I'm no expert in c++, just want to check the thread sleeping has no issue with you.

MisterD81 do you know does the CDM call timerfunc at the ~5 minute mark or is it at the start of the stream (called with 300000ms)?

@MisterD81
Copy link
Contributor Author

It is actually even a little bit more complicated. Initially you get keys that are valid for 5 minutes.
CDM requests a timer via "SetTimer" at startup, which is set in my case to 1-2 minutes (can't remember in detail the time). Once the timer is over in timerfunc we call CDM and inform that the timer has expired. CDM then triggers the license renewal via a specific CDM message (for which we have CheckLicenseRenewal). This will then set the timer again.
So you'll always have a timer running and should inform CDM when the CDM is expired. I have no clue if this timer is also used by CDM for other purposes then extending the license.

@@ -63,10 +64,18 @@ void* GetCdmHost(int host_interface_version, void* user_data)

} // namespace

std::atomic<bool> exit_thread_flag{false};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For simpler types regular assignment is easier to understand.

std::atomic<bool> exit_thread_flag = false;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved initialization to "Initialize()" as suggestion does not compile.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, of course. It's atomic bool. My bad.

std::this_thread::sleep_for(std::chrono::milliseconds(100));
waited += 100;
}
if (!exit_thread_flag){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing space flag) {

@@ -128,6 +137,8 @@ CdmAdapter::CdmAdapter(

CdmAdapter::~CdmAdapter()
{
exit_thread_flag = true;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This value appears arbitrary. What exactly is this waiting for?

This value could very well work on one system but not on another. It would be better to continue once a condition is satisfied.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added another flag to check if timer thread is running or not. The sleep is/was for ensuring that the timer is not running anymore as otherwise cdm would get destroyed to early.

@@ -307,6 +318,8 @@ void CdmAdapter::CloseSession(uint32_t promise_id,
const char* session_id,
uint32_t session_id_size)
{
exit_thread_flag = true;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This value appears arbitrary. What exactly is this waiting for?

This value could very well work on one system but not on another. It would be better to continue once a condition is satisfied.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

@MisterD81 MisterD81 requested a review from phunkyfish May 1, 2021 09:35
@matthuisman
Copy link
Contributor

matthuisman commented May 15, 2021

I rented a movie last night so can test. Only for next 48 hours tho

I'll pull in your PR over latest Matrix branch and test

@matthuisman
Copy link
Contributor

matthuisman commented May 15, 2021

@MisterD81 are you able to quickly run through what this pr is doing? Where does the sleep time come from? How does it know when its needed? Is this going to make all other content keep refreshing its license? Do we know why the code was commented out?

I see its mostly uncommenting code so hard to tell what the missing pieces are

@@ -307,6 +325,10 @@ void CdmAdapter::CloseSession(uint32_t promise_id,
const char* session_id,
uint32_t session_id_size)
{
exit_thread_flag = true;
while (timer_thread_running) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bracket on next line.

@@ -128,6 +140,10 @@ CdmAdapter::CdmAdapter(

CdmAdapter::~CdmAdapter()
{
exit_thread_flag = true;
while (timer_thread_running) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bracket on next line.

std::this_thread::sleep_for(std::chrono::milliseconds(100));
waited += 100;
}
if (!exit_thread_flag) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bracket on next line.

adp->TimerExpired(context);
timer_thread_running = true;
uint64_t waited = 0;
while (!exit_thread_flag && delay > waited) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bracket on next line.

@matthuisman
Copy link
Contributor

matthuisman commented May 16, 2021

ok tested. without this PR - YT movie stopped around 4:50 for me
and a bunch of DecodeVideo: kNoKey for key 43C36BB3877B5304913F9ED079E553EA messages

with this PR, I see a license request every minute and it played pass 5minutes without issue
Checked and the browser does same behavior (license request every minute)

@MisterD81
as far as I can tell, all the refresh stuff is done in the cdm itself?
Is it the response from the license server that maybe contains the refresh info?
Seems all the delay between refresh etc comes internally from the cdm.
then it must just call the same function that calls back into IA to get the license?

@anxdpanic
Not sure if this version of Youtube add-on is still being maintained, but noted 1x issue with a purchased movie.
its higher quality streams would pause after the non-encrypted first few segments.
Im pretty sure they need L1 device for playback. in the browser, the quality is limited to 480p.
If Youtube gives you that info (that it needs a certain widevine level), maybe you could exclude them if not Android device.
And if Android, then you'd have a new "L1 Device" setting. so that you can remove them on android if not L1.

Copy link
Contributor

@matthuisman matthuisman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good and works good!

just need those formatting changes fixed that phunky pointed out.
And maybe squash down into a single commit :)

@phunkyfish
Copy link
Contributor

@MisterD81 are you able to quickly run through what this pr is doing? Where does the sleep time come from? How does it know when its needed? Is this going to make all other content keep refreshing its license? Do we know why the code was commented out?

I see its mostly uncommenting code so hard to tell what the missing pieces are

I'm also curious on these questions.

@MisterD81 MisterD81 requested a review from phunkyfish May 16, 2021 12:01
@MisterD81
Copy link
Contributor Author

I adjusted the formatting.
I found that out also only by trial and error. This what I know from debugging also with the EME logger of Chrome:

  • YT gives you on some platforms only keys that are valid for around 5 minutes.
  • The CDM requests a timer and wants to be notified (CdmAdapter.SetTimer()). The wake up time itself comes from the CDM. So Chrome or ISA should notify the CDM that the timer is over. The time is for YT around 1 minute and is controlled by the CDM itstelf. Nothing actively. This code was already in ISA, but commented out.
  • Once the timer expires the CDM should be notified via TimerExpired(). Then SendClientMessage() will be triggered by the CDM which contains a renewal request in the end to the license server. Unfortunately the check for a new Key Challenge was commented out in wvdecrypter.cpp and the method CheckLicenseRenewal() which would check for a new challenge from the CDM would never get triggered. Once the new challenge is there it follows more or less the same procedure as initially. Of course CDM sets again a timer at the end and the process begins from the beginning.
  • The problematic part is that once you set the timer and would wait until the timer expires Kodi crashes when you stop playback. Perhaps someone could do it also with locks/guards and so on, but I decided to sleep only for a small period in the timerfunc to recognize that playback should stop. This way the timer gets stopped properly and Kodi stops crashing. Probably that was the reason why it was commented out. I found the commit when the code was introduced but it was introduced commented out.
  • Additionally keep in mind that most licenses that ISA is receiving do not expire that fast. They are not setting the timer. I checked it with Amazon for example and there the timer is not set. Also YT is doing that only on some platforms/CDM combinations. Unfortunately it seems that all RPi2/3 and Windows (x64) users are affected for the moment. So this is completely CDM driven based on what the license server is returning.
    Hope that helps.

@phunkyfish
Copy link
Contributor

Thanks for explaining. Did you know there is actually a Timer class available as part or the add-on API?

https://github.com/xbmc/xbmc/blob/master/xbmc/addons/kodi-dev-kit/include/kodi/tools/Timer.h

So you can just implement ITimerCallback in your class and have any number of timers.

Copy link
Contributor

@phunkyfish phunkyfish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for contributing!

@matthuisman
Copy link
Contributor

matthuisman commented May 16, 2021

@MisterD81

thanks for the explanation.
this is very nice - implementing a feature of the CDM itself.
This could also help any other add-ons that require refreshing the license (its just not Youtube specific)

I assume it was originally commented out due to the crashing issue that you fixed.
Good work!

@glennguy
this should be good to go in once you review :)

@glennguy glennguy merged commit 23146dc into xbmc:Matrix May 17, 2021
@MisterD81 MisterD81 deleted the License_Renewal_Matrix branch May 18, 2021 17:56
@matthuisman
Copy link
Contributor

I am finding this fixes Binge (and possible other addons) that now requires license renewal even on non-Android platforms.
@glennguy
maybe a candidate for Kodi 18 backport?

dobo90 pushed a commit to dobo90/inputstream.adaptive that referenced this pull request Feb 12, 2023
…hile CloseSession is active to avoid endless loop.

Sometimes when stopping a playback Kodi crashes with a following backtrace:

    (__m=<optimized out>, this=<optimized out>)
    at /usr/include/c++/12.1.0/bits/atomic_base.h:486
        __b = <optimized out>
    at /usr/include/c++/12.1.0/atomic:87
    (adp=std::shared_ptr<media::CdmAdapter> (use count 1, weak count 1) = {...}, delay=<optimized out>, context=<optimized out>)
    at /usr/src/debug/kodi-addon-inputstream-adaptive/inputstream.adaptive-20.3.3-Nexus/wvdecrypter/cdm/media/cdm/cdm_adapter.cc:81
    at /usr/include/c++/12.1.0/bits/invoke.h:96, unsigned long long, void*), std::shared_ptr<media::CdmAdapter>, long long, void*> >::_M_invoke<0u, 1u, 2u, 3u>(std::_Index_tuple<0u, 1u, 2u, 3u>)
    (this=<optimized out>) at /usr/include/c++/12.1.0/bits/std_thread.h:252
    at /usr/include/c++/12.1.0/bits/std_thread.h:259
    at /usr/include/c++/12.1.0/bits/std_thread.h:210
    at /usr/src/debug/gcc/libstdc++-v3/src/c++11/thread.cc:82
        ret = <optimized out>
        pd = 0x85cf9080
        unwind_buf = {cancel_jmp_buf = {{jmp_buf = {857629366, 66004830, -2049994624, -2008033272, -2008033282, 338, -2008033281, 8387456, -2058383360, -2049995908, 1920, 1080, 3840, 2160, 0, 10000000, -1431355392, 1107558643, 0 <repeats 46 times>}, mask_was_saved = 0}}, priv = {pad = {0x0, 0x0, 0x0, 0x0}, data = {prev = 0x0, cleanup = 0x0, canceltype = 0}}}
        not_first_call = <optimized out>
        robust = <optimized out>

After searching in the github history it came out that there was an
idenctical issue adressed in xbmc#589.

For some reason that piece of code got removed in
xbmc@8889fe9
(in pull request xbmc#883).

The stack trace is from the Raspberry Pi 4 machine. But I can also reproduce the issue
on my x86_64 Linux machine by adding a 1s sleep in the beginning of CdmAdapter::SetTimer.

In order to reproduce it is necessary to quickly play/stop streams.
Following script can be used to ease the reproduction. In my environment when
running following script for 1-3 minutes the screen freezes because of
the busy loop mentioned in xbmc#589.

After applying mentioned commit I'm not able to reproduce the issue anymore.

```
set -x

URI='Change it to some uri to play'

while true; do
  curl -s "http://kodi:[email protected]:8080/jsonrpc" -H 'Content-Type: application/json' --data "{\"jsonrpc\":\"2.0\",\"method\":\"Player.Open\",\"params\":{\"item\":{\"file\":\"$URI\"}}}"
  sleep $[ ($RANDOM % 70 + 30) / 10.0 ]

  curl -s "http://kodi:[email protected]:8080/jsonrpc" -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","method":"Player.Stop","params":{"playerid":1}}'
  sleep 1
done
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Issue Type: New feature issue has requested a new feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants