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 a "Use Vorbis" switch #151

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open

add a "Use Vorbis" switch #151

wants to merge 6 commits into from

Conversation

em1lyy
Copy link

@em1lyy em1lyy commented Nov 27, 2021

AAC is currently hard-coded as the codec used for transcoding. In
low-bandwidth environments, where transcoding is usually a must, AAC
performs very badly though, especially the built-in FFmpeg AAC encoder.
qaac and maybe FDK AAC are a bit better, but due to codec limitations,
even those high-performance codecs are outperformed by Vorbis on low
bitrates (also using those would require more patching in Jellyfin, and
using non-free software, so that wouldn't really work).
Vorbis is a free (as in freedom) audio format, and the libvorbis codec
implementation is also free (also as in freedom). Jellyfin supports
using it out-of-the-box.
While opus would've been a better choice, I and some friends currently
have some trouble getting it to work properly on the Jellyfin server
side of things, so I figured many other users do as well, which is why
Opus, even if it performs even better, hasn't been chosen.
A switch has been added into the Transcoding Settings Menu that toggles
between the widely-supported AAC and the higher-quality Vorbis. Settings
are stored in the Hive Database (I do not have Dart and Flutter
installed, so 1) this is untested and 2) the FinampModels.g.dart hasn't
been re-generated yet).

em1lyy and others added 4 commits November 27, 2021 15:26
AAC is currently hard-coded as the codec used for transcoding. In
low-bandwidth environments, where transcoding is usually a must, AAC
performs very badly though, especially the built-in FFmpeg AAC encoder.
qaac and maybe FDK AAC are a bit better, but due to codec limitations,
even those high-performance codecs are outperformed by Vorbis on low
bitrates (also using those would require more patching in Jellyfin, and
using non-free software, so that wouldn't really work).
Vorbis is a free (as in freedom) audio format, and the libvorbis codec
implementation is also free (also as in freedom). Jellyfin supports
using it out-of-the-box.
While opus would've been a better choice, I and some friends currently
have some trouble getting it to work properly on the Jellyfin server
side of things, so I figured many other users do as well, which is why
Opus, even if it performs even better, hasn't been chosen.
A switch has been added into the Transcoding Settings Menu that toggles
between the widely-supported AAC and the higher-quality Vorbis. Settings
are stored in the Hive Database (I do not have Dart and Flutter
installed, so 1) this is untested and 2) the FinampModels.g.dart hasn't
been re-generated yet).
@jmshrv
Copy link
Owner

jmshrv commented Nov 27, 2021

Thanks for this, just made a few changes:

  • Generated the Hive files and set a default value for useVorbis so that people updating will have the default value
  • Set the useVorbis option when creating a queue, and set the parameter that gets sent to Jellyfin to "libvorbis" (I'm assuming they just use the same names as ffmpeg?)
  • Hidden the button on iOS and macOS since Apple are too innovative™ to add Vorbis/OPUS support to the Apple audio stuff

For some reason, I can't get an audio output on the Android emulator, so I've attached a build below. Could you check that it works fine on your server?

release.zip

@em1lyy
Copy link
Author

em1lyy commented Nov 27, 2021

Oh, no, Jellyfin uses vorbis, there's some internal translation going on in the Jellyfin backend to do all of that (because audio format != codec used, esp. as many audio formats have multiple codecs in FFmpeg), the parameter being set to vorbis was fine (maybe that was why audio output wasn't working for you?)
And thank you for fixing the database stuff, I'm not really familiar with Dart, I tried my best to get into the codebase, but I just missed a few things

@jmshrv
Copy link
Owner

jmshrv commented Nov 27, 2021

For some reason AAC transcodes aren't playing any sound for me as well, I'll look into it tomorrow on a real device

@rom4nik
Copy link
Contributor

rom4nik commented Nov 28, 2021

Testing on a Xiaomi Mi 6 running LineageOS 18.1 (Android 11) I get the following errors with Vorbis transcoding enabled:

E/ExoPlayerImplInternal( 8073): Playback error
E/ExoPlayerImplInternal( 8073):   com.google.android.exoplayer2.ExoPlaybackException: Source error
E/ExoPlayerImplInternal( 8073):       at com.google.android.exoplayer2.ExoPlayerImplInternal.handleIoException(ExoPlayerImplInternal.java:624)
E/ExoPlayerImplInternal( 8073):       at com.google.android.exoplayer2.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:594)
E/ExoPlayerImplInternal( 8073):       at android.os.Handler.dispatchMessage(Handler.java:102)
E/ExoPlayerImplInternal( 8073):       at android.os.Looper.loop(Looper.java:223)
E/ExoPlayerImplInternal( 8073):       at android.os.HandlerThread.run(HandlerThread.java:67)
E/ExoPlayerImplInternal( 8073):   Caused by: com.google.android.exoplayer2.ParserException: Input does not start with the #EXTM3U header.
E/ExoPlayerImplInternal( 8073):       at com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser.parse(HlsPlaylistParser.java:258)
E/ExoPlayerImplInternal( 8073):       at com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser.parse(HlsPlaylistParser.java:68)
E/ExoPlayerImplInternal( 8073):       at com.google.android.exoplayer2.upstream.ParsingLoadable.load(ParsingLoadable.java:176)
E/ExoPlayerImplInternal( 8073):       at com.google.android.exoplayer2.upstream.Loader$LoadTask.run(Loader.java:409)
E/ExoPlayerImplInternal( 8073):       at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
E/ExoPlayerImplInternal( 8073):       at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
E/ExoPlayerImplInternal( 8073):       at java.lang.Thread.run(Thread.java:923)
E/AudioPlayer( 8073): TYPE_SOURCE: Input does not start with the #EXTM3U header.
I/ExoPlayerImpl( 8073): Release 5a5b05e [ExoPlayerLib/2.15.0] [sagit, MI 6, Xiaomi, 30] [goog.exo.core, goog.exo.hls]
I/flutter ( 8073): [MusicPlayerBackgroundTask/SEVERE] 2021-11-28 23:30:29.072575: (0) Source error
I/flutter ( 8073): [AudioServiceHelper/SEVERE] 2021-11-28 23:30:29.073566: (0) Source error
E/flutter ( 8073): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: (0) Source error

Vorbis transcodes succeed on server side, after a while the .ogg files are deleted though (I assume it's due to no playback progress reported):

[2021-11-28 23:41:31.215 +01:00] [INF] FFmpeg exited with code 0
[2021-11-28 23:41:37.091 +01:00] [INF] Transcoding kill timer stopped for JobId "f4e57b4f03a2431097d52c0a8b0eac1f" PlaySessionId "c10a88d5f4554864a7b64661a7c97cdd". Killing transcoding
[2021-11-28 23:41:37.091 +01:00] [INF] Deleting partial stream file(s) "/transcodes/3ea715b51c7be2a60fa5556d9e2a9bfa.ogg"

One time I've also experienced the player skipping through the queue, but I'm unable to reproduce this.
AAC transcodes work on my device.

@em1lyy
Copy link
Author

em1lyy commented Nov 29, 2021

Is this on the build provided in the .zip or the one after the libvorbisvorbis fix?
It might be that Jellyfin does not HLS Vorbis Audio, in which case we'd either need to tell ExoPlayer or change to the force HLS endpoint.

@rom4nik
Copy link
Contributor

rom4nik commented Nov 29, 2021

The second one. Lack of HLS could be the culprit, as AAC audio appears in transcodes directory as a bunch of .ts files and a .m3u8 playlist, meanwhile Vorbis audio is transcoded into single .ogg file.

@jmshrv
Copy link
Owner

jmshrv commented Nov 29, 2021

Yeah, the error is it complaining that it hasn't received proper HLS stuff. Does Vorbis support HLS? From what I can tell, HLS only supports AAC, MP3, AC-3 or EC-3 for audio. We could make it so that just_audio loads a regular audio source, but then we'd go back to seeking not working properly with transcoded audio. I'm considering making a gstreamer backend for just_audio, which may be able to handle non-HLS transcodes better (haven't actually tested, but Exoplayer/iOS don't do a great job so it's probably pretty easy to beat)

@em1lyy
Copy link
Author

em1lyy commented Nov 29, 2021

Non-transcoded audio is also not HLSed though, so, without having looked into the program logic too much, couldn't we just do whatever we're already doing with non-transcoded audio with Vorbis transcoded audio as well? If yes, that'd be a cleaner solution imo

@em1lyy
Copy link
Author

em1lyy commented Nov 29, 2021

Having looked into the code, the latest commit should also mitigate this by simply setting transcodingProtocol=http when useVorbis is true, and also creating the HlsAudioSource only if useVorbis is false.
Sorry for the untested code, again, I do not have Dart and Flutter installed on my system, probably should've installed those before submitting, I am sorry for the trouble

@rom4nik
Copy link
Contributor

rom4nik commented Nov 29, 2021

Now playback works, but seeking is broken - slider keeps moving forward from selected timestamp, but song is played from the start.

@em1lyy
Copy link
Author

em1lyy commented Nov 29, 2021

To me, that sounds like seeking into a region which isn't loaded on the device, either because of local buffering or server side transcoding progress. Not much that can be done about it (other than maybe resetting the slider), I'm afraid... Or are you getting a specific error from the seek function?
If not, one thing to try would be to prefer the position in milliseconds that the player knows over the value the slider thinks it has, which is currently the other way around.

@jmshrv
Copy link
Owner

jmshrv commented Nov 29, 2021

This is what I meant by "we'd go back to seeking not working properly with transcoded audio". From what I can tell, Jellyfin doesn't return a length for a transcoded file (because the file doesn't fully exist yet) and Exoplayer/iOS both rely on the length to handle seeking. HLS solves this by properly segmenting the audio, but that of course doesn't work with Vorbis. The proper way to handle seeking on non-HLS transcodes is to restart the stream at an offset (https://github.com/MediaBrowser/Emby/wiki/Audio-Streaming#seeking), which may be possible but it would be very jank. We could add a disclaimer that seeking won't work when Vorbis transcoding is enabled, but that's a bit lazy.

Random note - transcoding currently performs like this in the release build, I really need to get 0.6 out lol

@em1lyy
Copy link
Author

em1lyy commented Nov 29, 2021

This might actually seem like this is true at first glance. However, Ogg Vorbis NEVER encodes duration information - thus none is written, both for transcoded and direct-played audio. You can easily verify this by wget'ing some transcoded endpoint from your Jellyfin server and grabbing some full Ogg Vorbis file, and then doing head -n1000 file.ogg | ffprobe - - both will say

Input #0, ogg, from 'pipe:':
  Duration: N/A

ExoPlayer waits for the End of Stream (EOS) packet and determines the length using that - and so do FFmpeg and many others.
In the end, this means that ExoPlayer handles transcoded Ogg Vorbis files the same way it handles slowly loading Ogg Vorbis files - not seeking for a few seconds, then everything seeks just fine. As the file is sent in transcoding real-time by Jellyfin, Finamp and ExoPlayer have the full Ogg Vorbis file as soon as the transcoding process is done. On a Raspberry Pi 4, this takes about 5-6 seconds for a 5 minute FLAC file. So in the end, seeking should not work for a few seconds but do fine after that. I tried to build this version, but couldn't set everything up in time for this comment, but I'm 99% sure that, in theory, things should be like that.
A little note could be added that states "Seeking does not work for the first ~5 seconds when Vorbis is enabled", but that feature would really mean a lot to me audio quality-wise, no problem though if these first few seconds of the track are too big of a problem ^^

@jmshrv
Copy link
Owner

jmshrv commented Dec 29, 2021

Just tested it in an emulator and seeking restarts the stream even if the stream has completed. This is likely an issue with ExoPlayer. IMO, this is fine as long as we just add a note saying that seeking will not work. I'm considering writing a GStreamer backend for just_audio, which may handle seeking better.

@jmshrv
Copy link
Owner

jmshrv commented Jan 28, 2023

As for Opus not working from Jellyfin, it looks like it never specified the codec when transcoding, so if you were requesting an OGG it would default to Vorbis.

jellyfin/jellyfin#9192

This PR is probably too stale to really merge now, and I'm currently working on transcoded downloads which may change how this PR could be implemented. If my Jellyfin PR gets merged, it would make sense to add extra options, such as Opus, as we'll finally be able to stream Opus on iOS through the CAF container.

@jmshrv
Copy link
Owner

jmshrv commented Jul 31, 2023

Just did a quick test, Jellyfin can return OPUS from a HLS stream, makes this a no-brainer for Android (assuming it can play Opus-encoded HLS). I'll look into updating this PR at some point, hopefully before this PR turns two years old 🙃

james@JamesLaptop /tmp> curl "http://my-server:8096/Audio/0d811359188af113961e7d18b0085ca7/hls1/main/0.ts?audioCodec=opus&audioBitRate=128000&ApiKey=notsaying&runtimeTicks=0&actualSegmentLengthTicks=30000000" > test.ts
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 49632  100 49632    0     0   811k      0 --:--:-- --:--:-- --:--:--  881k
james@JamesLaptop /tmp> mediainfo test.ts 
General
ID                                       : 1 (0x1)
Complete name                            : test.ts
Format                                   : MPEG-TS
File size                                : 48.5 KiB
Duration                                 : 2 s 900 ms
Overall bit rate mode                    : Variable
Overall bit rate                         : 131 kb/s

Audio
ID                                       : 256 (0x100)
Menu ID                                  : 1 (0x1)
Format                                   : Opus
Codec ID                                 : 6
Compression mode                         : Lossy
descriptor_tag_extension                 : 128

@Chaphasilor
Copy link
Collaborator

hopefully before this PR turns two years old 🙃

you have about two months left 😁

@jmshrv
Copy link
Owner

jmshrv commented Oct 4, 2023

haha I'll probably roll it into #496 🙃

@Chaphasilor
Copy link
Collaborator

Okay, so the new download system already supports transcoded downloads using OPUS, and it's really impressive how little bitrate it needs. We probably need to extend the scale to support even lower values...
I'm guessing we should be able to add OPUS transcoding support now for regular playback, and given that @jmshrv's PRs have been merged, it should also work fine on iOS.

@Chaphasilor
Copy link
Collaborator

Currently working on #915 which changes the default segment container from MPEG-TS to Fragmented MP4 (fMP4). This seems to improve codec compatibility. This might also make OPUS or Vorbis viable for regular transcoded streaming on Android and iOS.

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.

4 participants