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

Add NewPipeExtractor for stream extraction #1772

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

Conversation

javdc
Copy link
Contributor

@javdc javdc commented Dec 22, 2024

Use NewPipeExtractor as a method to obtain song info and stream URLs. Currently this is only a proof of concept.

Advantages

  • When YouTube changes something on their end, we would just need to wait for NewPipe team to fix it and update the library in InnerTune, they usually do it really fast, in a matter of hours or a couple of days.

Disadvantages

  • Dependency on another library, but at least it only adds one more MB of size to the APK.
  • NewPipeExtractor doesn't return some data, such as the song loudness and sample rate, so this info will be missing in details and volume normalization won't work.
  • Slightly more data usage, NewPipeExtractor makes more requests than we need, for example to obtain related videos. I haven't found a way to configure this.

To do

  • Right now this PR uses NewPipeExtractor as first option just to help testing, but the idea is to only use it as a fallback.
  • There should be a way to try other stream extraction methods when one returns a seemingly valid url or signatureCipher but the stream doesn't work (for example, the server returns 403). Currently, the stream extraction and the song info database saving are all done in Media3's DataSource.Factory, so this feature would have to be developed in another PR. As an idea: maybe we could do a HTTP HEAD request before returning the url to the player to see if it responds with a 200 or not?
  • I think Piped fallback should be removed, all public Piped instances are not working due to YouTube actively blocking their IPs. See Got error: "Sign in to confirm that you're not a bot" TeamPiped/Piped#3658. This should also be done in another PR.

I need testers! Please try this with a YouTube account and age restricted content. I only tried it without logging in, my account is not age verified and I won't give Google my ID card just to try this. I added the login cookie to NewPipeExtractor requests, so I think it should work, but I'm not sure.

@javdc javdc marked this pull request as draft December 22, 2024 19:12
@javdc javdc changed the title [DRAFT] Add NewPipeExtractor for stream extraction Add NewPipeExtractor for stream extraction Dec 22, 2024
@javdc javdc mentioned this pull request Dec 22, 2024
4 tasks
@fishmodem
Copy link

fishmodem commented Dec 22, 2024

Lots of duplicates of the issue, so I'm gonna mention them so they all get a mention back here and we get more testers: #1753 #1754 #1757 #1758 #1760 #1764 #1765 #1770

For people who want a briefing on this:

It seems like this issue is an API change on youtube's side, specific to users logged into a youtube account. Logging out fixes playback, but not for age-restricted content. For people reporting that the debug build and outertune works, it's just working because haven't tried logging into their accounts. In the case of outertune the root issue is not fixed, but login appears to work because of their hotfix 0.6.4:

"Please note that this uses the "logged out" method of song playback for ALL song playback, and as a result, content that you need to be logged in for (and possibly age-restricted content) would be inaccessible."

tldr: If you'd like to try and help test the fallback implemented above, you can download the auto-generated debug build which contains it:
https://github.com/z-huang/InnerTune/actions/runs/12456971205/artifacts/2353818490

@fishmodem
Copy link

fishmodem commented Dec 22, 2024

It works fine with non-age-restricted content while logged in now, but attempting to stream age-restricted content (While logged in with 18+ google account) crashes the app. Digging through logcat now to find out why. Never tried debugging something like this before so lmk if you know of a better method and I'll post whatever debug info I can scrounge up. For reference I'm on lineageOS using microg so it's possible this issue is specific to my device setup, but considering that the login feature worked fine before it's likely this crash will happen similarly on other devices.

@fishmodem
Copy link

fishmodem commented Dec 22, 2024

The errors of note:

12-22 14:32:51.251 30458 30479 W System.err: org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException: This age-restricted video cannot be watched.
12-22 14:32:51.251 30458 30479 W System.err: 	at org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor.onFetchPage(YoutubeStreamExtractor.java:819)
12-22 14:32:51.251 30458 30479 W System.err: 	at org.schabi.newpipe.extractor.Extractor.fetchPage(Extractor.java:60)
12-22 14:32:51.251 30458 30479 W System.err: 	at org.schabi.newpipe.extractor.stream.StreamInfo.getInfo(StreamInfo.java:77)
12-22 14:32:51.251 30458 30479 W System.err: 	at org.schabi.newpipe.extractor.stream.StreamInfo.getInfo(StreamInfo.java:72)
12-22 14:32:51.251 30458 30479 W System.err: 	at com.zionhuang.innertube.YouTube.player-0E7RQCE(YouTube.kt:444)
12-22 14:32:51.251 30458 30479 W System.err: 	at com.zionhuang.innertube.YouTube.player-0E7RQCE$default(YouTube.kt:442)
12-22 14:32:51.252 30458 30479 W System.err: 	at com.zionhuang.music.playback.MusicService$createDataSourceFactory$1$playerResponse$1.invokeSuspend(MusicService.kt:642)
12-22 14:32:51.252 30458 30479 W System.err: 	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
12-22 14:32:51.252 30458 30479 W System.err: 	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
12-22 14:32:51.252 30458 30479 W System.err: 	at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:115)
12-22 14:32:51.252 30458 30479 W System.err: 	at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:103)
12-22 14:32:51.252 30458 30479 W System.err: 	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
12-22 14:32:51.252 30458 30479 W System.err: 	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
12-22 14:32:51.252 30458 30479 W System.err: 	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
12-22 14:32:51.252 30458 30479 W System.err: 	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
12-22 14:32:51.750 30458 30550 E LoadTask: Unexpected exception loading stream
12-22 14:32:51.750 30458 30550 E LoadTask:   androidx.media3.common.PlaybackException: This video may be inappropriate for some users.
12-22 14:32:51.750 30458 30550 E LoadTask:       at com.zionhuang.music.playback.MusicService.createDataSourceFactory$lambda$36(MusicService.kt:657)
12-22 14:32:51.750 30458 30550 E LoadTask:       at com.zionhuang.music.playback.MusicService.$r8$lambda$mEm3ksxFlHMtAunGpHpRoCpwVY8(Unknown Source:0)
12-22 14:32:51.750 30458 30550 E LoadTask:       at com.zionhuang.music.playback.MusicService$$ExternalSyntheticLambda5.resolveDataSpec(D8$$SyntheticClass:0)
12-22 14:32:51.750 30458 30550 E LoadTask:       at androidx.media3.datasource.ResolvingDataSource.open(ResolvingDataSource.java:108)
12-22 14:32:51.750 30458 30550 E LoadTask:       at androidx.media3.datasource.StatsDataSource.open(StatsDataSource.java:86)
12-22 14:32:51.750 30458 30550 E LoadTask:       at androidx.media3.exoplayer.source.ProgressiveMediaPeriod$ExtractingLoadable.load(ProgressiveMediaPeriod.java:1045)
12-22 14:32:51.750 30458 30550 E LoadTask:       at androidx.media3.exoplayer.upstream.Loader$LoadTask.run(Loader.java:421)
12-22 14:32:51.750 30458 30550 E LoadTask:       at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137)
12-22 14:32:51.750 30458 30550 E LoadTask:       at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)
12-22 14:32:51.750 30458 30550 E LoadTask:       at java.lang.Thread.run(Thread.java:1012)

Then exo player then throws an exception (androidx.media3.exoplayer.ExoPlaybackException: Source error) cause the media source wasn't set up properly, then firebase tries to handle it and crashes the app because apparently it wasn't set up properly: AndroidRuntime: Shutting down VM ... java.lang.IllegalStateException: Default FirebaseApp is not initialized in this process com.zionhuang.music.debug. Make sure to call FirebaseApp.initializeApp(Context) first.

Not sure if the firebase error is because I didn't set up some debugging tool on my end, but it looks like the real issue lies in the newpipe extractor being unable to play the age-restricted content. The same video plays without login in newpipe so I figure it's possible to work around.

@javdc
Copy link
Contributor Author

javdc commented Dec 22, 2024

Thank you for testing! Well, it seems adding the login cookie to the requests isn't enough for age restricted videos, unfortunately. About the last crash, I don't think it's a problem on your end, it's probably a problem with the GitHub action that builds the apk, I don't think Crashlytics should be trying to send crashes in testing builds anyway...

But you say that the age restricted video is playing for you in NewPipe? Can you put the video link here?

@gechoto
Copy link
Contributor

gechoto commented Dec 23, 2024

it seems adding the login cookie to the requests isn't enough

Only adding the cookie does nothing.
You also have to add the Authorization header like here:

append("Authorization", "SAPISIDHASH ${currentTime}_${sapisidHash}")

Now the problem is that this way of sending the login only works for requests with the web client. But NewPipeExtractor makes two requests:

  • One for metadata with the web client (login works)
  • One for the streams with the IOS client (doesn't like login)

@javdc
Copy link
Contributor Author

javdc commented Dec 23, 2024

Now the problem is that this way of sending the login only works for requests with the web client. But NewPipeExtractor makes two requests:

  • One for metadata with the web client (login works)
  • One for the streams with the IOS client (doesn't like login)

Ooh I didn't know that, then I guess this solution won't work for age restricted songs. It could still be useful as a fallback when the native methods from InnerTune don't work, but seeing that right now InnerTune also uses IOS client to get the streams I'm not sure if it's worth adding NewPipeExtractor as a fallback...

@gechoto
Copy link
Contributor

gechoto commented Dec 23, 2024

this solution won't work for age restricted songs

as well as for all other things that need login which is a notable downgrade

since the IOS client does not support login anymore (or nobody knows how) the logic should be:

if (userLoggedIn) {
  // use web client (for everything including the streams)
  // (maybe use ios client only as fallback)
} else {
  // use ios client
}

But I don't know how to tell NewPipeExtractor to use a specific client. getWebPlayerResponse is hard-coded to always use the web player only for metadata. Sure we could write a wrapper and modify the requests to include streams but...

@javdc what do you think about this:

  • Make IOS or WEB_REMIX request directly with InnerTune (depending on if the user is logged in)
  • Use NewPipeExtractor only to deobfuscate signatures in case we don't get direct urls

This way we also still have all the metadata and normalization will still work.

I think this could be tested in a separate PR. I would like to keep this one as kind of an archive in case we need this approach later.

@gechoto
Copy link
Contributor

gechoto commented Dec 24, 2024

@javdc new PR: #1774

I used some of your code for it, hope this is fine

@javdc
Copy link
Contributor Author

javdc commented Dec 24, 2024

@javdc what do you think about this:

  • Make IOS or WEB_REMIX request directly with InnerTune (depending on if the user is logged in)
  • Use NewPipeExtractor only to deobfuscate signatures in case we don't get direct urls

This way we also still have all the metadata and normalization will still work.

Just so you know, PR #1690 already handles signature deobfuscation, but it also uses clients that don't work anymore. If that PR is updated I think we could do everything natively

@javdc new PR: #1774

I used some of your code for it, hope this is fine

No problem!

@gechoto
Copy link
Contributor

gechoto commented Dec 24, 2024

Just so you know, PR #1690 already handles signature deobfuscation, but it also uses clients that don't work anymore. If that PR is updated I think we could do everything natively

Even with updated clients the urls produced by this PR don't work anymore.
Maybe the signature deobfuscation logic was changed.
Do you know how to update it?

I guess using NewPipeExtractor for deobfuscation will be easier to maintain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants