Jump to content

Rotated videos force full software transcode on NVIDIA. HW decode is dropped, playback fails.


Recommended Posts

danergo
Posted

Environment

  • Emby Server 4.9.5.0, Linux (Ubuntu, kernel 7.0.0-38-generic), x64
  • Bundled ffmpeg: 5.1-emby_2023_06_25_p4
  • GPU: NVIDIA Quadro P400 (Pascal, CC 6.1), detected correctly. NVDEC/CUVID HEVC decoders and NVENC H.264 encoder are all listed and pass checks.
  • Hardware acceleration mode: Advanced
  • Client: Emby Web 4.9.5.0, Chrome on Windows, remote connection

Source file

  • DJI Osmo Action 4 recording, MP4, 3840x2160, HEVC Main 10 (yuv420p10le), 50 fps, ~110 Mbit/s, Level 150, SDR/BT.709
  • The camera was mounted upside down, so the file carries rotation metadata:
  Side data:
    displaymatrix: rotation of -180.00 degrees

Problem

Playback fails with “No compatible streams are currently available.” Non-rotated 4K HEVC files from the same camera transcode fine on the same server with full NVDEC → NVENC.

The transcode does start, but runs at about 6 fps (`speed=0.11x`) for a 50 fps source. The client gives up and stops it (`[q] command received`):

elapsed=00:00:09.54 frame=58 fps=6.1 ... speed=0.111x
[q] command received. Exiting.

Analysis

The processing plan shows rotation is not supported in hardware:

Name                                CanDoInHW  WillDoInHW
NVDEC Quadro P400 - H.265 (HEVC) >> True       True
VideoInput                       >> True       False
Rotation                         >> False      False
Scaling                          >> True       True
ColorConversion                  >> True       True
VideoOutput                      >> True       True
NVENC Quadro P400 - H.264 (AVC)  >> True       True

The resulting filter chain rotates in software before uploading to CUDA:

HEVC >> rotate >> hwupload_cuda >> scale_cuda >> setsar

The plan says NVDEC will be used, but the actual ffmpeg command line uses the software HEVC decoder limited to a single thread. There is no `-hwaccel cuda` and no `hevc_cuvid`:

-c:v:0 hevc -threads:v:0 1 -noautorotate ...
-filter_complex "[0:0]rotate@f1=angle=180*(PI/180),hwupload_cuda@f2,scale_cuda@f3=w=1920:h=1080:format=yuv420p,setsar@f4=sar=sar[f4_out0]"

So a single CPU thread has to decode 4K 10-bit HEVC at 110 Mbit/s, and then a generic software `rotate` filter runs on every 4K 10-bit frame. NVENC encoding itself works fine. The bottleneck is entirely the forced software decode plus software rotation.

Why this seems unnecessary

  1. The hardware can do it. A 180° rotation is just a horizontal plus vertical flip. The decoded frame is already in GPU memory after NVDEC, so a trivial CUDA operation (or NPP, e.g. `transpose_npp`) could handle it without leaving the GPU.
  2. Even if rotation must stay in software, the fallback could be much better. The pipeline could still decode on NVDEC, `hwdownload`, flip on the CPU, and `hwupload_cuda` again. Failing that, it could at least use a multi-threaded software decoder instead of `-threads 1`.
  3. The software `rotate` filter is a poor choice for 180°. It is a general arbitrary-angle rotation with interpolation. `hflip,vflip` (or `transpose`-based equivalents) would be considerably cheaper for 90/180/270° cases.

Requests

  • Add hardware rotation support for NVIDIA (CUDA/NPP-based flip/transpose), at least for 90/180/270°, so rotated videos can stay on a full HW pipeline.
  • Until then, when rotation forces a software step, keep hardware decoding and only download/upload around the rotation step, or don’t limit the software decoder to one thread.
  • Use `hflip`/`vflip`/`transpose` instead of the generic `rotate` filter for right-angle rotations.

This affects a very common use case: action cameras (DJI, GoPro) and phones often record with rotation metadata, especially when mounted upside down.

Workaround for now: re-encode the affected files outside Emby, so the rotation is baked into the pixels and the metadata tag is removed.

Full transcode log available on request.

danergo
Posted

Hi @Luke, do you need anything from my side? Please let me know.

  • 2 weeks later...
Posted

Hi, yes currently it is a generalized rotation. You are correct that certain rotation angles have specific functions that we can use to optimize this. That's something we can look at using. Thanks.

danergo
Posted
22 minutes ago, Luke said:

Hi, yes currently it is a generalized rotation. You are correct that certain rotation angles have specific functions that we can use to optimize this. That's something we can look at using. Thanks.

Thank you!

@softworkzalso confirmed this as for the future transcoding update possibilities:

 

Posted

@danergo

What you are seeing (threads 1, no hwaccel) is a bug in current Emby server.

What is intended to happpen is to have the Nvidia decoder output the frames to system memory directly - because this is faster than outputting to CUDA hw context and then doing hwdownload.
It appears though, that this path has only been tested with cuvid but not with nvdec decoders. For the latter, this leads to sw decoding - not by intention, that's why threads=1 is used, still assuming it  would do hw decoding.


What this means for you is this: in current stable and beta yuo should already be able to get hw decoding by using HEVC_CUVID instead. To do this, go to transcoding settings in advanced mode, and then, where the HEVC decoders are, move hevc_cuvid above hevc_nvdec. 

Save and try again!

Posted
On 10/14/2026 at 4:13 PM, danergo said:

The software `rotate` filter is a poor choice for 180°. It is a general arbitrary-angle rotation with interpolation. `hflip,vflip` (or `transpose`-based equivalents) would be considerably cheaper for 90/180/270° cases.


In this regard, you are not quite right.

The rotate filter does allow rotation by arbitrary angles, but it has special paths for 0,90,180,270° which are doing a fast and exact transformation.

The rotate filter performance is equal to vflip+hflip - normally would it be even much faster, because it does the 180° in a single go, whereas hflip+vflip are too separate filters and would normally need to move around the image data twice.

But it's not like that, because the hflip filter is using a "dirty trick". hflip doesn't change the image at all - it just redefines the data representation:

  • It changes the line pointers to point at the end of each row (instead of the start)
  • then it negates the line length, so for example for an image with a line length length (stride) of 1024 it would change the definition to -1024

Only because of this trick, the two-filter combination is even a viable option, but not substantially faster than rotate as you can see for yourself:

ffmpeg -hide_banner -benchmark -filter_complex "testsrc2=size=3840x2160:rate=60000/1001,format=yuv420p10le,loop=loop=-1:size=50:start=0,rotate=angle=180*(PI/180):fillcolor=none[v]" -map "[v]" -t 20 -c:v wrapped_avframe -f null NUL
ffmpeg -hide_banner -benchmark -filter_complex "testsrc2=size=3840x2160:rate=60000/1001,format=yuv420p10le,loop=loop=-1:size=50:start=0,hflip,vflip[v]" -map "[v]" -t 20 -c:v wrapped_avframe -f null NUL

 

You can verify what I said by using vflip,vflip and hflip,hflip

 

PS: One thing you might notice - the rotate filter has fillcolor=none specified. This is missing in the curtrent Emby Server but makes a big difference to performance.
For the time being, you can use the Diagnostics plugin to configure an ffmpeg command modification  (like replace rotate= by rotate=fillcolor=none: ) 

 

danergo
Posted

@softworkz:

I got just a bit confused now: shall I try reorder the decoders first, then try adding fillcolor? 

Posted
5 minutes ago, danergo said:

@softworkz:

I got just a bit confused now: shall I try reorder the decoders first, then try adding fillcolor? 

Yes, one should give you hw decoding and the second will accelerate the rotate filter.

danergo
Posted

Ah got it.

First, your test "ffmpeg commands", 1st one:

Stream mapping:
  rotate:default -> Stream #0:0 (wrapped_avframe)
Press [q] to stop, [?] for help
Output #0, null, to 'NUL':
  Metadata:
    encoder         : Lavf62.3.100
  Stream #0:0: Video: wrapped_avframe, yuv420p10le(progressive), 3840x2160 [SAR 1:1 DAR 16:9], q=2-31, 200 kb/s, 59.94 fps, 59.94 tbn
    Metadata:
      encoder         : Lavc62.11.100 wrapped_avframe
frame=   16 fps=0.0 q=-0.0 size=N/A time=00:00:00.26 bitrate=N/A speed=0.533x elframe=   35 fps= 35 q=-0.0 size=N/A time=00:00:00.58 bitrate=N/A speed=0.584x elframe=   56 fps= 37 q=-0.0 size=N/A time=00:00:00.93 bitrate=N/A speed=0.623x elframe=  122 fps= 61 q=-0.0 size=N/A time=00:00:02.03 bitrate=N/A speed=1.02x elaframe=  191 fps= 76 q=-0.0 size=N/A time=00:00:03.18 bitrate=N/A speed=1.27x elaframe=  261 fps= 87 q=-0.0 size=N/A time=00:00:04.35 bitrate=N/A speed=1.45x elaframe=  329 fps= 94 q=-0.0 size=N/A time=00:00:05.48 bitrate=N/A speed=1.57x elaframe=  397 fps= 99 q=-0.0 size=N/A time=00:00:06.62 bitrate=N/A speed=1.66x elaframe=  468 fps=104 q=-0.0 size=N/A time=00:00:07.80 bitrate=N/A speed=1.73x elaframe=  537 fps=107 q=-0.0 size=N/A time=00:00:08.97 bitrate=N/A speed=1.79x elaframe=  606 fps=110 q=-0.0 size=N/A time=00:00:10.11 bitrate=N/A speed=1.84x elaframe=  675 fps=112 q=-0.0 size=N/A time=00:00:11.26 bitrate=N/A speed=1.88x elaframe=  745 fps=115 q=-0.0 size=N/A time=00:00:12.42 bitrate=N/A speed=1.91x elaframe=  815 fps=116 q=-0.0 size=N/A time=00:00:13.59 bitrate=N/A speed=1.94x elaframe=  884 fps=118 q=-0.0 size=N/A time=00:00:14.74 bitrate=N/A speed=1.97x elaframe=  954 fps=119 q=-0.0 size=N/A time=00:00:15.91 bitrate=N/A speed=1.99x elaframe= 1024 fps=120 q=-0.0 size=N/A time=00:00:17.08 bitrate=N/A speed=2.01x elaframe= 1094 fps=122 q=-0.0 size=N/A time=00:00:18.23 bitrate=N/A speed=2.03x elaframe= 1163 fps=122 q=-0.0 size=N/A time=00:00:19.40 bitrate=N/A speed=2.04x ela[out#0/null @ 0x60c9ff3111c0] video:487KiB audio:0KiB subtitle:0KiB other streams:0KiB global headers:0KiB muxing overhead: unknown
frame= 1199 fps=122 q=-0.0 Lsize=N/A time=00:00:20.00 bitrate=N/A speed=2.04x elapsed=0:00:09.82
bench: utime=26.138s stime=0.661s rtime=9.825s
bench: maxrss=1311936KiB

 

Second:

Stream mapping:
  vflip:default -> Stream #0:0 (wrapped_avframe)
Press [q] to stop, [?] for help
Output #0, null, to 'NUL':
  Metadata:
    encoder         : Lavf62.3.100
  Stream #0:0: Video: wrapped_avframe, yuv420p10le(progressive), 3840x2160 [SAR 1:1 DAR 16:9], q=2-31, 200 kb/s, 59.94 fps, 59.94 tbn
    Metadata:
      encoder         : Lavc62.11.100 wrapped_avframe
frame=   20 fps=0.0 q=-0.0 size=N/A time=00:00:00.33 bitrate=N/A speed=0.667x elframe=   43 fps= 43 q=-0.0 size=N/A time=00:00:00.71 bitrate=N/A speed=0.717x elframe=  135 fps= 90 q=-0.0 size=N/A time=00:00:02.25 bitrate=N/A speed= 1.5x elaframe=  258 fps=129 q=-0.0 size=N/A time=00:00:04.30 bitrate=N/A speed=2.15x elaframe=  380 fps=152 q=-0.0 size=N/A time=00:00:06.33 bitrate=N/A speed=2.53x elaframe=  502 fps=167 q=-0.0 size=N/A time=00:00:08.37 bitrate=N/A speed=2.79x elaframe=  625 fps=178 q=-0.0 size=N/A time=00:00:10.42 bitrate=N/A speed=2.98x elaframe=  748 fps=187 q=-0.0 size=N/A time=00:00:12.47 bitrate=N/A speed=3.12x elaframe=  870 fps=193 q=-0.0 size=N/A time=00:00:14.51 bitrate=N/A speed=3.22x elaframe=  993 fps=198 q=-0.0 size=N/A time=00:00:16.56 bitrate=N/A speed=3.31x elaframe= 1115 fps=202 q=-0.0 size=N/A time=00:00:18.60 bitrate=N/A speed=3.38x ela[out#0/null @ 0x63c5023c2100] video:487KiB audio:0KiB subtitle:0KiB other streams:0KiB global headers:0KiB muxing overhead: unknown
frame= 1199 fps=203 q=-0.0 Lsize=N/A time=00:00:20.00 bitrate=N/A speed=3.38x elapsed=0:00:05.91
bench: utime=16.393s stime=0.634s rtime=5.917s
bench: maxrss=1312448KiB

As for reordering the decoders, please find attached my transcode log. Summary: video starts, but can't play smooth, it buffers around every few seconds.

Second file is done with the params added in Diag options.

Dear @softworkz, if you have any additions/suggestions or questions, let me know it.

ffmpeg-transcode-0e87eb7a-1479-4ede-8127-78de4387e1cd_1.txt 2nd_with_replaced_params-ffmpeg-transcode-974118d2-8a5c-43fc-86d3-74450bb0b0ef_1.txt

Posted

Oh, sorry, you need to change 

need rotate@f1=

to

rotate@f1=fillcolor=none:

But I'm not sure whether this will suffice to get over 1.0x speed..

The hw decoding is working at least.

Posted

Of course you can also try the flips, which would be:

rotate@f1=angle=180*(PI/180)

to

vflip,hflip

Posted

Looking at your results above, the flips are 1.7x faster - on my system, they were almost equal, but I ran it with the new ffmpeg, I haven't traced back what happened to those filters  since then.

So, better go with thre flips!

danergo
Posted
5 hours ago, softworkz said:

Oh, sorry, you need to change 

need rotate@f1=

to

rotate@f1=fillcolor=none:

But I'm not sure whether this will suffice to get over 1.0x speed..

The hw decoding is working at least.

Tried, see the first attachment (starting with "1.ffmpeg..."): still lagging too much.

5 hours ago, softworkz said:

Of course you can also try the flips, which would be:

rotate@f1=angle=180*(PI/180)

to

vflip,hflip

Tried too, see the second attachment (starting with "2.ffmpeg..."): plays smooth (although it's barely above 1.0x).

1.ffmpeg-transcode-fa4070cd-9194-4ae7-a750-c007439aa9ee_1.txt 2.ffmpeg-transcode-f381b657-4fea-45ed-9493-39706aa68fef_1.txt

danergo
Posted

Some more numbers from my system (Quadro P400, same 4K 10-bit HEVC 50 fps clip, recent FFmpeg 8.x):

 

Pipeline fps CPU time for the 30s clip
hevc_cuvid decode only (to system memory) 118 9.2s
cuvid → vflip,hflip → hwupload_cuda → scale_cuda → nvenc ~63 avg (~80 steady) 45.3s
full GPU: nvdec (cuda frames) → scale_cuda → nvenc, no rotation 113 2.3s

 

So the full GPU pipeline runs at the decoder’s limit, with almost no CPU use. The software flip plus the 4K download/upload costs roughly 20x more CPU and about half the throughput.

A CUDA flip/transpose (e.g. transpose_cuda, LGPL, now in FFmpeg master) would let rotated videos stay on the full GPU path at about 110 fps instead of ~50. For now the vflip,hflip replacement works for me (1.02x), but with very little headroom.

Posted
1 minute ago, danergo said:

A CUDA flip/transpose (e.g. transpose_cuda, LGPL, now in FFmpeg master) would let rotated videos stay on the full GPU path at about 110 fps instead of ~50.

Yes, that's clearly much better. It's not really the filter performance in that case, it's the data transfer from disk to gpu then back to sysmem then filtering, then movign data to gpu mem again and finally from gpu mem back to disk.

HW transpose for CUDA is implemented already. It will be in the beta from the first day on.

danergo
Posted

Thank you very much for the quick process! Once beta is ready, I'll give it another test run with the same file and repost here the log.

  • Like 1
Posted

If you're on the beta then I would try the latest build as it has rotation adjustments.

Posted
3 hours ago, Luke said:

If you're on the beta then I would try the latest build as it has rotation adjustments.

Without hw transpose, it cannot get better than what we tried already.

  • Thanks 1
danergo
Posted

@softworkz, @Luke:

Current beta is 4.11.0.12. Does it contain the hw_transpose for CUDA?

If not, can you tell which exact beta should I look for?

 

Thx!

Posted
10 hours ago, danergo said:

@softworkz, @Luke:

Current beta is 4.11.0.12. Does it contain the hw_transpose for CUDA?

If not, can you tell which exact beta should I look for?

 

Thx!

No sorry not that one specifically. I forgot it was cuda you were asking about. That one depends on the new ffmpeg build.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...