jellyfin-android
jellyfin-android copied to clipboard
Add bitrate/quality menu to ExoPlayer
Also modified ExoPlayer profile to take into account the highest quality profile(?) supported when transcoding.
Before
{
"Type": "Video",
"Conditions": [
{
"Condition": "LessThanEqual",
"Property": "VideoBitrate",
"Value": "120000000",
"IsRequired": true
},
{
"Condition": "EqualsAny",
"Property": "VideoProfile",
"Value": "constrained baseline|baseline|main|constrained high|high",
"IsRequired": true
},
{
"Condition": "LessThanEqual",
"Property": "VideoLevel",
"Value": "52",
"IsRequired": true
}
],
"Codec": "h264",
"Container": null
}
x264 [error]: invalid profile: constrainedbaseline [libx264] Error setting profile constrainedbaseline. [libx264] Possible profiles: baseline main high high10 high422 high444
After
{
"Type": "Video",
"Conditions": [
{
"Condition": "LessThanEqual",
"Property": "VideoBitrate",
"Value": "120000000",
"IsRequired": true
},
{
"Condition": "EqualsAny",
"Property": "VideoProfile",
"Value": "high|constrained high|main|baseline|constrained baseline",
"IsRequired": true
},
{
"Condition": "LessThanEqual",
"Property": "VideoLevel",
"Value": "52",
"IsRequired": true
}
],
"Codec": "h264",
"Container": null
}
Successful transcoding
See https://github.com/jellyfin/jellyfin/issues/4659#issuecomment-739869592
Closes #202
Changes not tested in version 10.7, maybe fixes required.
Bonus point for you if you manage to use the same player instance when switching between the different quality options.
Closes #268, #24
Done, added QualityOptions implementation in Kotlin for 10.7, feel free to modify it as needed; I tried to make it an accurate copy of the JS so there are some things that are not used (like the maxHeight property in video, and all the audio quality options themselves).
The bitrate selector is always Auto even if the bitrate is already specified in the playback settings page. But the bitrate setting does take effect.
The bitrate selector is always Auto even if the bitrate is already specified in the playback settings page. But the bitrate setting does take effect.
I suppose, since I am not taking into account the option selected in the configurations for now, since it is possible to select a quality in the server (jf-web) that does not exist in the client (QualityOptions.kt), but I will see what I can do.
For now you can test the quality selector by playing a video with ExoPlayer, inside you should see an icon of a gear, that is the menu that adds this PR.
For now you can test the quality selector by playing a video with ExoPlayer, inside you should see an icon of a gear, that is the menu that adds this PR.
The quality selector works except for the bug I mentioned above.
Here are my two new discoveries: Lower the bitrate while direct playing to force transcode in exoplayer will report two sessions from this android client. This can be observed in dashboard. Directplay and transcoding will appear alternately.
The bitrate setting should not affect the external player. But now setting a lower bitrate will prevent the external player from launching.
Here are my two new discoveries: Lower the bitrate while direct playing to force transcode in exoplayer will report two sessions from this android client. This can be observed in dashboard. Directplay and transcoding will appear alternately.
This is likely because ExoPlayer reports progress both via the (native) API client and the web app (API client). So, it's not the fault of this PR, I'm just not sure whether we'll be able to fix this somehow. I suppose it's a bug that's acceptable until we have a native app.
The bitrate setting should not affect the external player. But now setting a lower bitrate will prevent the external player from launching.
Unfortunately there is a hardcoded part in jf-web that doesn't allow me to separate the bitrate settings, at least not until the playback code is native, this means that some changes were required for ExternalPlayer...
also have to try it myself on-device since this is a rather big PR
Un(fortunately) this grew as I was "fixing" things, so good luck reviewing this...
New changes:
- Modified ExoPlayer profile (Reorganized the list of compatible subtitles, added a maxWidth and maxHeight condition to each video codec)
- Modified ExternalPlayer profile (ExoPlayer changes + Transcoding Profiles)
- Follow the max bitrate settings in jf-web
- Follow the maxHeight condition when playing a video if applicable (downscale media)
- Allow transcoding in ExternalPlayer
Some bugs that I tried to fix:
- Pressing the "Select Server" button several times can throw a NullPointerException if multiple dialogs are shown at the same time, disable the button to avoid this behavior, and re-enable if no server is selected.
- goBack() does not work correctly in 10.7 (example dialog in Media -> Details -> Media Info does not close with the Android return action, only with the icon in the upper left part of jf-web)
And some probably important unresolved bugs:
- Repeatedly exiting and entering picture-in-picture mode in NativePlayer duplicates the time updates sometimes
- Resetting(?) the server in the middle of the playback crash the application
2020-12-20 13:00:08.794 24167-24167/org.jellyfin.mobile.debug I/chromium: [INFO:CONSOLE(2)] "web socket closed", source: https://demo.jellyfin.org/unstable/web/main.bundle.js (2)
2020-12-20 13:00:08.795 24167-24167/org.jellyfin.mobile.debug I/chromium: [INFO:CONSOLE(2)] "Clearing KeepAlive for [object WebSocket]", source: https://demo.jellyfin.org/unstable/web/main.bundle.js (2)
2020-12-20 13:00:08.795 24167-24167/org.jellyfin.mobile.debug I/chromium: [INFO:CONSOLE(2)] "nulling out web socket", source: https://demo.jellyfin.org/unstable/web/main.bundle.js (2)
2020-12-20 13:00:12.293 24167-24458/org.jellyfin.mobile.debug D/Volley: [313405] BasicNetwork.logSlowRequests: HTTP response for request=<[ ] https://demo.jellyfin.org/unstable/Sessions/Playing/Progress 0xfe27ff0e NORMAL 35> [lifetime=3540], [size=10587], [rc=500], [retryCount=0]
2020-12-20 13:00:12.294 24167-24458/org.jellyfin.mobile.debug E/Volley: [313405] BasicNetwork.performRequest: Unexpected response code 500 for https://demo.jellyfin.org/unstable/Sessions/Playing/Progress
2020-12-20 13:00:12.298 24167-24167/org.jellyfin.mobile.debug E/TimberLogger: VolleyError com.android.volley.ServerError: null
com.android.volley.ServerError
at com.android.volley.toolbox.BasicNetwork.performRequest(BasicNetwork.java:205)
at com.android.volley.NetworkDispatcher.processRequest(NetworkDispatcher.java:131)
at com.android.volley.NetworkDispatcher.processRequest(NetworkDispatcher.java:111)
at com.android.volley.NetworkDispatcher.run(NetworkDispatcher.java:90)
2020-12-20 13:00:12.300 24167-24167/org.jellyfin.mobile.debug E/ContinuationEmptyResponse: org.jellyfin.apiclient.model.net.HttpException: VolleyError com.android.volley.ServerError:
at org.jellyfin.apiclient.interaction.VolleyErrorListener.onErrorResponse(VolleyErrorListener.java:25)
at com.android.volley.Request.deliverError(Request.java:617)
at com.android.volley.ExecutorDelivery$ResponseDeliveryRunnable.run(ExecutorDelivery.java:104)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:491)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:940)
Caused by: com.android.volley.ServerError
at com.android.volley.toolbox.BasicNetwork.performRequest(BasicNetwork.java:205)
at com.android.volley.NetworkDispatcher.processRequest(NetworkDispatcher.java:131)
at com.android.volley.NetworkDispatcher.processRequest(NetworkDispatcher.java:111)
at com.android.volley.NetworkDispatcher.run(NetworkDispatcher.java:90)
2020-12-20 13:00:12.384 24167-24167/org.jellyfin.mobile.debug E/RedScreenOfDeath: An error occurred in the uncaught exception handler
java.lang.RuntimeException: Parcelable encountered IOException writing serializable object (name = org.jellyfin.apiclient.model.net.HttpException)
at android.os.Parcel.writeSerializable(Parcel.java:1833)
at android.os.Parcel.writeValue(Parcel.java:1780)
at android.os.Parcel.writeArrayMapInternal(Parcel.java:928)
at android.os.BaseBundle.writeToParcelInner(BaseBundle.java:1584)
at android.os.Bundle.writeToParcel(Bundle.java:1253)
at android.os.Parcel.writeBundle(Parcel.java:997)
at android.content.Intent.writeToParcel(Intent.java:10498)
at android.app.IActivityTaskManager$Stub$Proxy.startActivity(IActivityTaskManager.java:3823)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1712)
at android.app.ContextImpl.startActivity(ContextImpl.java:957)
at android.app.ContextImpl.startActivity(ContextImpl.java:928)
at android.content.ContextWrapper.startActivity(ContextWrapper.java:383)
at com.melegy.redscreenofdeath.RedScreenOfDeath.handleUncaughtException(RedScreenOfDeath.kt:26)
at com.melegy.redscreenofdeath.RedScreenOfDeath.access$handleUncaughtException(RedScreenOfDeath.kt:10)
at com.melegy.redscreenofdeath.RedScreenOfDeath$init$crashListener$1.onUncaughtException(RedScreenOfDeath.kt:13)
at com.melegy.redscreenofdeath.internal.UncaughtExceptionHandler.uncaughtException(UncaughtExceptionHandler.kt:9)
at org.chromium.base.JavaExceptionReporter.uncaughtException(chromium-TrichromeWebViewGoogle.aab-stable-428010133:6)
at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1073)
at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1068)
at kotlinx.coroutines.android.AndroidExceptionPreHandler.handleException(AndroidExceptionPreHandler.kt:47)
at kotlinx.coroutines.CoroutineExceptionHandlerImplKt.handleCoroutineExceptionImpl(CoroutineExceptionHandlerImpl.kt:29)
at kotlinx.coroutines.CoroutineExceptionHandlerKt.handleCoroutineException(CoroutineExceptionHandler.kt:33)
at kotlinx.coroutines.StandaloneCoroutine.handleJobException(Builders.common.kt:189)
at kotlinx.coroutines.JobSupport.finalizeFinishingState(JobSupport.kt:229)
at kotlinx.coroutines.JobSupport.tryMakeCompletingSlowPath(JobSupport.kt:903)
at kotlinx.coroutines.JobSupport.tryMakeCompleting(JobSupport.kt:860)
at kotlinx.coroutines.JobSupport.makeCompletingOnce$kotlinx_coroutines_core(JobSupport.kt:825)
at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:111)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46)
at kotlinx.coroutines.internal.DispatchedContinuation.resumeWith(DispatchedContinuation.kt:188)
at kotlin.coroutines.SafeContinuation.resumeWith(SafeContinuationJvm.kt:42)
at org.jellyfin.mobile.utils.ContinuationEmptyResponse.onError(ApiExtensions.kt:91)
at org.jellyfin.apiclient.interaction.Response.onError(Response.java:32)
at org.jellyfin.apiclient.interaction.Response.onError(Response.java:32)
at org.jellyfin.apiclient.interaction.ApiClientRequestListener.onError(ApiClientRequestListener.java:37)
at org.jellyfin.apiclient.interaction.VolleyErrorListener.onErrorResponse(VolleyErrorListener.java:36)
at com.android.volley.Request.deliverError(Request.java:617)
at com.android.volley.ExecutorDelivery$ResponseDeliveryRunnable.run(ExecutorDelivery.java:104)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:491)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:940)
Caused by: java.io.NotSerializableException: com.android.volley.NetworkResponse
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1240)
at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1604)
2020-12-20 13:00:12.385 24167-24167/org.jellyfin.mobile.debug E/RedScreenOfDeath: at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1565)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1488)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1234)
at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1604)
at java.io.ObjectOutputStream.defaultWriteObject(ObjectOutputStream.java:463)
at java.lang.Throwable.writeObject(Throwable.java:1027)
at java.lang.reflect.Method.invoke(Native Method)
at java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:1036)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1552)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1488)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1234)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:354)
at android.os.Parcel.writeSerializable(Parcel.java:1828)
... 44 more
2020-12-20 13:00:12.388 24167-24167/org.jellyfin.mobile.debug I/in.mobile.debu: System.exit called, status: 1
2020-12-20 13:00:12.388 24167-24167/org.jellyfin.mobile.debug I/AndroidRuntime: VM exiting with result code 1, cleanup skipped.
- Crash on startup in Jellyfin Debug
Jellyfin Debug crashed in main thread
Version code: 1
Version name: 0.0.0-dev.1
Stack Trace:
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at androidx.fragment.app.FragmentManager.checkStateLoss(FragmentManager.java:1703)
at androidx.fragment.app.FragmentManager.enqueueAction(FragmentManager.java:1743)
at androidx.fragment.app.BackStackRecord.commitInternal(BackStackRecord.java:321)
at androidx.fragment.app.BackStackRecord.commit(BackStackRecord.java:286)
at org.jellyfin.mobile.MainActivity$onCreate$1$invokeSuspend$$inlined$collect$1.emit(Collect.kt:149)
at kotlinx.coroutines.flow.StateFlowImpl.collect(StateFlow.kt:342)
at kotlinx.coroutines.flow.StateFlowImpl$collect$1.invokeSuspend(Unknown Source:12)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:227)
at kotlinx.coroutines.DispatchedTaskKt.resumeUnconfined(DispatchedTask.kt:190)
at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:161)
at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:362)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:396)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:388)
at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:293)
at kotlinx.coroutines.flow.StateFlowSlot.makePending(StateFlow.kt:230)
at kotlinx.coroutines.flow.StateFlowImpl.updateState(StateFlow.kt:295)
at kotlinx.coroutines.flow.StateFlowImpl.setValue(StateFlow.kt:262)
at org.jellyfin.mobile.viewmodel.MainViewModel.refreshServer(MainViewModel.kt:39)
at org.jellyfin.mobile.viewmodel.MainViewModel$refreshServer$1.invokeSuspend(Unknown Source:11)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:491)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:940)
We've dropped all 10.6 code from the app, which should make it easier for this PR to be reviewed too. Can you rebase the PR? In the assets the "native-10.7" folder is now called "native" and the "native-10.6" folder was removed.
Did a quick rebase onto the latest master, now on to reviewing this!
Finally ready to review after another rewrite, unfortunately the PR got bigger, and since it is a hassle to go around switching between branches, then everything I have to contribute I put together here, good luck!
New changes:
- Dropped the code for external player from this PR
- Rebased to current master
- Added profile condictions for DeviceProfileBuilder
- Added video quality options custom preference (@Maxr1998 It's your library, I'm probably not using it correctly)
- Fixed session integration with webapp
* Added playSessionId for MediaSourceResolver
* Now it report playback propertly to the server
* But I had to "disable" some of the existing code related to the ApiController
* ApiController.baseDeviceInfo.id != NativeInterface.getDeviceInformation().deviceInfo.id
- Changed js injection method
* Now is injected properly every time - no more undefined NativeShell
* Fixed again the blank screen error if server offline
- request.url needs that server.hostname ends whih "/"
- Load backdrop image if available in ExoPlayer
- Add option to remember brightness changes in ExoPlayer
- Stop active encoding if transcoding when playbacks ends
* Now is possible to reinitialize the current playback, needed for apply changes in transcodes.
- Changed PLAYER_TIME_UPDATE_RATE from 1 second to 10 seconds
* Too much spam before, to the level of slowing down the server, also is the default in jellyfin-web
Feel free to add, edit or remove anything you want from this PR
Thanks for the rework! I planned to do this myself, but I haven't gotten around yet, so you beat me! :grin: I'm (unfortunately) again busy with university and work, so I can't properly review it at the moment, but from a quick glance the code looks quite good. I would've preferred to get some of the features as individual PRs, but I can cherry-pick those myself if you don't want to. But as I said, it'll take a while until I get to do that.