Skip to content

stereo3d: detect layout signalled in the video stream (frame packing SEI, st3d) - #18490

Merged
kasper93 merged 5 commits into
mpv-player:masterfrom
danielcamposramos:stereo3d-frame-packing-detection
Sep 23, 2026
Merged

kasper93 merged 5 commits into
mpv-player:masterfrom
danielcamposramos:stereo3d-frame-packing-detection

Conversation

@danielcamposramos

@danielcamposramos danielcamposramos commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #18489.
What this patches:

  • mpv detects a stereo 3D layout only from the Matroska StereoMode element. A layout signalled in the video stream, the H.264/HEVC frame_packing_arrangement SEI (payload type 45), or the MP4 st3d box, is ignored, and such files play as stereo-in=mono.

libavcodec already decodes that SEI into AVStereo3D frame side data and hands it to mpv. mpv never asks for it.

Reproduction steps:
Three files isolating each signal, built in three commands using ffmpeg:

ffmpeg -f lavfi -i "testsrc2=size=960x1080:rate=24:duration=2" \
       -f lavfi -i "smptebars=size=960x1080:rate=24:duration=2" \
       -filter_complex "[0:v][1:v]hstack=inputs=2[v]" -map "[v]" \
       -c:v libx264 -crf 20 -pix_fmt yuv420p source_sbs.mp4

ffmpeg -i source_sbs.mp4 -c:v copy -an plain.mkv                      # no signal
mkvmerge -o container_only.mkv --stereo-mode 0:1 plain.mkv            # container tag only
ffmpeg -i source_sbs.mp4 -c:v libx264 -crf 20 -pix_fmt yuv420p \
       -x264-params frame-packing=3 sei_only.mp4                      # in-band SEI only
for f in plain.mkv container_only.mkv sei_only.mp4; do
  printf '%-20s ' "$f"
  mpv --no-config --vo=null --frames=1 --no-audio \
      --term-playing-msg='${video-params}' "$f" 2>/dev/null | grep -i stereo-in
done

Before this patch sei_only.mp4 reports stereo-in=mono; after it reports stereo-in=sbs2l.

Why the first frame was not enough:
The in-band signal is attached only to the frames that carry it. Measured files.

Results
Verified on Debian 14, x86_64, libavcodec 63.1.101, libplacebo 7.360.1, built from this tree. Real files are ordinary 3D videos, not synthesized for the test.

No container-tagged file changes behaviour, and files with no signal stay mono.
The inverted case is the one that showed why metadata beats guessing: that file's SEI declares
side by side (inverted), the right eye first type, while its filename says SBS.

Before this change mpv showed it flat; a player guessing from the filename would swap the viewer's eyes. Only the declared layout gets it right.

Scope:
This makes mpv detect the layout. It does not change what mpv does with it, no automatic conversion is added, and --vf=format:stereo-in=... still overrides as before.

Files that already worked are unaffected.

Relation to existing issues:

Why this is becoming more common:

HandBrake merged support for writing this SEI (PR #8100), and FFmpeg has an open request for a lossless injector for it (#24531), so files carrying the in-band signal are expected to keep appearing.

Testing:
Tested, per DOCS/contribute.md. Built from this tree and run against the three control files and a 26-file library of real 3D videos. Happy to adjust the mapping's placement — it could live next to mp_stereo3d_names[]incsputils.c` instead of inline — or to split the fallback differently if you would rather the container tag always win.

Disclosure:
An AI partner helped produce the outcomes. The words here are mine, edited in the browser.

Every result above was executed and measured on my machine, not inferred;
The patch was compiled, the before/after values come from running both binaries against the same files, and the side-data-per-keyframe counts were measured with ffmpeg -vf showinfo.

@danielcamposramos

Copy link
Copy Markdown
Contributor Author

Follow-up on scope, before it comes up in review.

The description says no container-tagged file changes behaviour. That holds when the container tag and the in-band signal agree, which is the normal case, but not when they disagree:

mkvmerge -o conflict.mkv --stereo-mode 0:3 sei_only.mp4   # tag: top-bottom, SEI: side-by-side

  before: stereo-in=ab2l    (container tag)
  after:  stereo-in=sbs2l   (in-band signal)

This is deliberate — it mirrors the frame rotation handling a few lines below in fix_image_params(), where the frame's value also takes priority over the container — but it is a behaviour change on a tagged file, so it should be stated rather than implied. If you would prefer the container tag to always win when both are present, that is a one-line reordering and I am happy to make it.

@CounterPillow

Copy link
Copy Markdown
Contributor
mucho texto

@danielcamposramos

Copy link
Copy Markdown
Contributor Author
mucho texto

Why are you so rude? Your single accepted PR is larger and contains no technical nothing, it's nothing but bad words on people trying to collaborate because, yes, it's a people dream (humans project into others what they are) to be listed as contributor, not to actually contribute with fixes....

@CounterPillow

Copy link
Copy Markdown
Contributor

Your single accepted PR

$ gol --author="Nicolas F" | wc -l
28

?

The rude part here is you letting clod flood the maintainers with redundant text.

@llyyr

llyyr commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Why are you so rude? Your single accepted PR is larger and contains no technical nothing, it's nothing but bad words on people trying to collaborate because, yes, it's a people dream (humans project into others what they are) to be listed as contributor, not to actually contribute with fixes....

Please read the contribution guidelines. You need to show you're capable of responding to reviews with human-written responses, which the AI-slop written commit messages implies you're not.

@danielcamposramos

Copy link
Copy Markdown
Contributor Author

Why are you so rude? Your single accepted PR is larger and contains no technical nothing, it's nothing but bad words on people trying to collaborate because, yes, it's a people dream (humans project into others what they are) to be listed as contributor, not to actually contribute with fixes....

Please read the contribution guidelines. You need to show you're capable of responding to reviews with human-written responses, which the AI-slop written commit messages implies you're not.

AI-slop - define this term, please? What about human slop, like being rude to a genuine contribuition proposal? You are assuming that because I leveraged AI to write the PR is slop? HandBreak actually verifyed my claims before that...

This message might contain english errors because it's not my main language and mainteners prefer wrong english than AI assisted writting.

Comment thread video/mp_image.c Outdated
Comment thread video/mp_image.c Outdated
Comment thread video/mp_image.c Outdated
Comment thread video/mp_image.c Outdated
Comment thread video/mp_image.c Outdated
Comment thread video/mp_image.c Outdated
Comment thread filters/f_decoder_wrapper.c Outdated
Comment thread filters/f_decoder_wrapper.c Outdated
@danielcamposramos

Copy link
Copy Markdown
Contributor Author

How often the frame-packing SEI is actually attached to a frame?
Runnable evidence for the "why the first frame was not enough": the frame_packing_arrangement SEI reaches libavcodec as AV_FRAME_DATA_STEREO3D on keyframes only, so a player that reads the side data once and then lets fix_image_params() run again on the next frame loses the layout.
That is what the PR's second commit (f_decoder_wrapper: keep in-band stereo 3D layout across frames) fixes.

It needs no patched mpv and no sample downloads.
Stock ffmpeg, four commands, about ten seconds.

The block

# Build a 10 s SBS clip with the frame-packing SEI and a keyframe every 48 frames.
ffmpeg -v error -f lavfi -i "testsrc2=size=960x1080:rate=24:duration=10" \
       -f lavfi -i "smptebars=size=960x1080:rate=24:duration=10" \
       -filter_complex "[0:v][1:v]hstack=inputs=2[v]" -map "[v]" \
       -c:v libx264 -crf 20 -pix_fmt yuv420p -g 48 \
       -x264-params frame-packing=3 sei_multi.mp4

# Same thing without the SEI, as a negative control.
ffmpeg -v error -f lavfi -i "testsrc2=size=960x1080:rate=24:duration=10" \
       -f lavfi -i "smptebars=size=960x1080:rate=24:duration=10" \
       -filter_complex "[0:v][1:v]hstack=inputs=2[v]" -map "[v]" \
       -c:v libx264 -crf 20 -pix_fmt yuv420p -g 48 no_sei.mp4

# How many frames actually carry AV_FRAME_DATA_STEREO3D?
for f in sei_multi.mp4 no_sei.mp4; do
  ffprobe -v error -select_streams v:0 -show_frames "$f" > /tmp/f.$$
  printf "%-16s frames=%-5s keyframes=%-4s stereo3d_side_data=%s\n" "$f" \
    "$(grep -c '^media_type=video'          /tmp/f.$$)" \
    "$(grep -c '^key_frame=1'               /tmp/f.$$)" \
    "$(grep -c '^side_data_type=Stereo 3D'  /tmp/f.$$)"
  rm -f /tmp/f.$$
done

# And which frames they are.
ffprobe -v error -select_streams v:0 -show_frames sei_multi.mp4 | awk '
  /^media_type=video/{n++} /^key_frame=1/{k[n]=1} /^side_data_type=Stereo 3D/{s[n]=1}
  END{for(i=1;i<=n;i++) if(k[i]||s[i]) printf "frame %3d  keyframe=%d  stereo3d=%d\n", i, (k[i]?1:0), (s[i]?1:0)}'

Output

sei_multi.mp4    frames=240   keyframes=5    stereo3d_side_data=5
no_sei.mp4       frames=240   keyframes=5    stereo3d_side_data=0
frame   1  keyframe=1  stereo3d=1
frame  49  keyframe=1  stereo3d=1
frame  97  keyframe=1  stereo3d=1
frame 145  keyframe=1  stereo3d=1
frame 193  keyframe=1  stereo3d=1

Same counts and the same frame indices across three major FFmpeg versions, so the result is a property of the file rather than of one build.

Why it is built this way?
Two details do real work and are easy to leave out.
The negative control. no_sei.mp4 is the same clip encoded without -x264-params frame-packing=3 and reports stereo3d_side_data=0. Without it, a reader cannot tell whether the counter detects the SEI or simply always prints a number.
The keyframe interval. -g 48 over 10 seconds gives five keyframes. A 2-second clip has exactly one, and 1 of 1 cannot distinguish "one per keyframe" from "one per file" — which is precisely the distinction the second commit turns on. Frames 1, 49, 97, 145 and 193 settle it: the side data lands on every keyframe and on nothing else, consistent with frame_packing_arrangement_repetition_period = 1 ("persists until the next IDR").

@danielcamposramos

Copy link
Copy Markdown
Contributor Author

Another data point on the approach, in case it is useful to the review: Kodi already does this, and has for years.

CDVDVideoCodecFFmpeg takes the stereo mode from the decoded frame's metadata — FFmpeg's H.264 decoder sets a stereo_mode entry from the frame-packing SEI, with the Matroska strings as values (left_right, top_bottom, …):

xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecFFmpeg.cpp:1043
AVDictionaryEntry* entry = av_dict_get(m_pFrame->metadata, "stereo_mode", NULL, 0);

Verified by running Kodi 21.2 on a file whose only 3D signal is the SEI (neutral filename, no container tag): autodetected stereo mode for movie mode left_right.

Sample to check against:

ffmpeg -f lavfi -i testsrc2=size=1920x1080:rate=24 -t 10 -c:v libx264 \
  -x264-params frame-packing=3 -pix_fmt yuv420p sbs.mp4
ffprobe -show_frames -read_intervals %+#1 sbs.mp4 | grep side_data_type   # Stereo 3D
ffprobe -show_entries stream_tags=stereo_mode sbs.mp4                     # nothing

And on why it matters outside players: on 2011–2012 Sony BRAVIA sets this SEI is the only signal that makes the display engage 3D by itself.

Measured through two independent DLNA servers, byte-exact: the SEI-carrying file engages 3D, the identical file with the SEI removed plays flat.

@danielcamposramos

Copy link
Copy Markdown
Contributor Author

Look, I am not that good with words and english can be complicated to convey, I therefore disclosured the leverage of AI on the PR. The new rule also landed one day after I filed the PR and merge request.
Please, do not stay out of the 3D video playback fix just because I am Brazilian and used AI to help write some text, look at the content for what it is and think on all future generations that will enjoy seamless 3D content on VR and old school 3D hardware.

@danielcamposramos

Copy link
Copy Markdown
Contributor Author

@kasper93 urging for a neutral technical review here.

@kasper93 kasper93 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, the report is valid and the first commit is the right idea. Keep in mind what mpv does with this value: it places OSD and subtitles per eye for side by side and top bottom layouts, and it is shown in video-params. There is no conversion, so the scope is a lot smaller than the description suggests.

The second commit is workaround for a libavcodec bug, and should not be added here.

What I would like to see:

  • Commit 1 with the opaque_ref comment fixed.
  • Commit 2 reduced to if (!m.stereo3d) m.stereo3d = p->codec->stereo_mode;.
  • The persistence fixed in libavcodec. ff_h2645_sei_to_frame() already does it for the film grain SEI, frame packing needs the same treatment.
  • If you want a fallback for older FFmpeg inside mpv, it has to live in vd_lavc and must not be cleared in reset_params.

Please trim the description and the commit messages to what the code does, we don't need so much of backstory for trivial changes.

Comment thread video/mp_image.c Outdated
Comment thread video/mp_image.c Outdated
Comment thread video/mp_image.c Outdated
Comment thread filters/f_decoder_wrapper.c Outdated
Comment thread filters/f_decoder_wrapper.c Outdated
Comment thread filters/f_decoder_wrapper.c
Comment thread filters/f_decoder_wrapper.c Outdated
@danielcamposramos
danielcamposramos force-pushed the stereo3d-frame-packing-detection branch from 1c5c04f to fc2ac9e Compare September 21, 2026 22:56
@danielcamposramos

Copy link
Copy Markdown
Contributor Author

v2 is up, reworked to the review point by point.

What changed

  1. The mp_image commit keeps the mapping and fixes what was flagged: the comment now describes the AV_FRAME_DATA_STEREO3D side data the code actually reads (opaque_ref is written by mp_image_to_av_frame() and is NULL on the decode path), QUINCUNX maps to 1/11 since it is half-width side-by-side for OSD purposes, and the AV_STEREO3D_2D case carries an explicit comment that 0 also means "no signal" in the params vocabulary — an inherited conflation, not introduced here.
  2. The wrapper commit is now only the guarded container fallback: if (!m.stereo3d) m.stereo3d = p->codec->stereo_mode; The persistence field, the reapply block and the reset clearing are gone.
  3. Persistence moves to libavcodec, where it belongs. A separate FFmpeg patch will give frame packing the same treatment film grain already gets in ff_h2645_sei_to_frame(), and it fixes both directions of the bug: H.264 drops the signal per access unit, HEVC never clears it. Registered as follow-up, deliberately not in this PR.
  4. No mpv-side fallback for old FFmpeg in v2. If compatibility with older FFmpeg is wanted, it lives in vd_lavc and is not cleared in reset_params; say the word and it is one paragraph.

Description and commit messages trimmed to what the code does; the LLM-assistance disclosure stays, per contribute.md.

Behavior, honestly
On unpatched FFmpeg an SEI-only file now reports the layout on IDR cadence: frames carrying the SEI get it, the ones between report no signal until the FFmpeg fix lands. Container-tagged files behave exactly as before. That is the price of not working around libavcodec in mpv, and the fixture table in the original description already quantified the cadence.

Precedent
The same read-the-SEI direction just landed upstream in MKVToolNix (!6311, AVC merged; HEVC in review as !6312): at mux time, when the container says nothing, probe the stream. This PR is the matching move at display time.

Issue impact
Closes #18489. Unblocks #17632 and #18348 without closing them: stats and OSD get a correct stereo-in on SEI/st3d files and the layout signal becomes reliable, while full vs half remains container-dependent and the stats formatting question stays with stats.lua.

The fixtures are reproducible in a minute from the ffmpeg commands in the description, if anyone wants to verify the behavior locally; nothing proprietary, no footage.

@danielcamposramos

Copy link
Copy Markdown
Contributor Author

@kasper93 The libavcodec persistence fix you asked for is up for review as FFmpeg PR #24628: frame packing now gets the same treatment as film grain in ff_h2645_sei_to_frame(), for H.264, HEVC and VVC.

@alexandre-janniaux

Copy link
Copy Markdown

Hi from VideoLAN contributor! 🫡 We received the exact same contribution on the VLC side and you'd be surprise to learn the prompt for all those merge requests is partially available on the contributor upstream: https://github.com/danielcamposramos/sony-bravia-linux/blob/main/ai-skill/SKILL.md

I've forked it there too:https://github.com/alexandre-janniaux/sony-bravia-linux/blob/main/ai-skill/SKILL.md

You'll find such text in the prompt, showing how much this has been automated:

Upstream posts

    One bug per issue, and keep it short. Maintainers read many issues. Say what happens, why it is wrong, what you measured and what you propose, then stop. Links and evidence go in attachments, not in the body.
    Pace matters. Several good posts in a short span are still a flood. Hold finished work until the current thread has settled.
    Follow each project's own rules. Mailing lists get plain-text mail threaded with the right headers; trackers get their templates; a project that bans AI mentions in commit messages gets its rule honoured.
    Accept correct review, including from bots. When an automated reviewer is right, fix it and thank it in the thread. When it is wrong, say what was measured.
    Posts go out in Daniel's own words. Drafts are raw material. Nothing public goes out under Daniel's name without Daniel's approval.

Access and sources

    If a site blocks automated access, stop. Do not change the user agent or route around it. Ask a human to open the page.
    Check content, not status codes. A block or a dead page can answer HTTP 200. Read what came back.

Best regards,

@danielcamposramos

danielcamposramos commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Hi from VideoLAN contributor! 🫡 We received the exact same contribution on the VLC side and you'd be surprise to learn the prompt for all those merge requests is partially available on the contributor upstream: https://github.com/danielcamposramos/sony-bravia-linux/blob/main/ai-skill/SKILL.md

I've forked it there too:https://github.com/alexandre-janniaux/sony-bravia-linux/blob/main/ai-skill/SKILL.md

You'll find such text in the prompt, showing how much this has been automated:

Upstream posts

    One bug per issue, and keep it short. Maintainers read many issues. Say what happens, why it is wrong, what you measured and what you propose, then stop. Links and evidence go in attachments, not in the body.
    Pace matters. Several good posts in a short span are still a flood. Hold finished work until the current thread has settled.
    Follow each project's own rules. Mailing lists get plain-text mail threaded with the right headers; trackers get their templates; a project that bans AI mentions in commit messages gets its rule honoured.
    Accept correct review, including from bots. When an automated reviewer is right, fix it and thank it in the thread. When it is wrong, say what was measured.
    Posts go out in Daniel's own words. Drafts are raw material. Nothing public goes out under Daniel's name without Daniel's approval.

Access and sources

    If a site blocks automated access, stop. Do not change the user agent or route around it. Ask a human to open the page.
    Check content, not status codes. A block or a dead page can answer HTTP 200. Read what came back.

Best regards,

  1. That file is a guide for contributors to my repository, like a CONTRIBUTING file. It's not the prompt of this PR.

  2. It was first written on 23 September at 02:59 UTC (link: danielcamposramos/sony-bravia-linux@d1c9e7d). This PR was opened on 16 September, and my review replies date from 16 to 21 September, before the file existed.

  3. AI assistance was disclosed from the start, and the code was changed exactly as kasper93 asked. The FFmpeg half he requested is up as FFmpeg PR #24628.

  4. Please judge the diff.

The only source of the stereo 3D layout was the Matroska StereoMode
element read by demux_mkv. libavcodec attaches the layout signaled in
the bitstream, such as the H.264 and HEVC frame packing arrangement
SEI, or by the container, such as the MP4 st3d box, to the decoded
frame as AV_FRAME_DATA_STEREO3D. Map it onto mp_image_params.stereo3d.

Frames coming back from libavfilter keep mpv's own value restored from
opaque_ref.

Co-authored-by: Kacper Michajłow <kasper93@gmail.com>
fix_image_params() unconditionally overwrote the layout with the
container tag, so the value mp_image_from_av_frame() reads from
AV_FRAME_DATA_STEREO3D never reached the output. Fall back to the tag
only when the bitstream signals nothing, the same rule rotation already
follows.

Fixes: mpv-player#18489
Co-authored-by: Kacper Michajłow <kasper93@gmail.com>
@kasper93
kasper93 force-pushed the stereo3d-frame-packing-detection branch from 3bcee81 to e2e62db Compare September 23, 2026 13:20
set_params() treated 0 as unset, but 0 is mono, so stereo-in=mono was
a no-op and the only way to drop a stereo 3D mode was stereo-in=no,
which wrote the invalid value -1 into the image params. Use -1 as the
unset default like rotate does, so no keeps the source mode and mono
overrides it.
mpv stopped inserting vf_stereo3d itself in a5610b2, the names only
need to stay recognizable next to the stereo3d filter documentation that
DOCS/man/vf.rst points to. The filter also accepts icl and icr as input
formats.
The last caller went away with the rpi code in 343a5fd. It also
regenerated the OSD with stereo mode 0, so any new user would have
reset the per eye OSD layout on stereo 3D content. Drop the change
tracking state that only existed for it.
@kasper93
kasper93 merged commit bdefd6c into mpv-player:master Sep 23, 2026
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.

Stereo 3D layout signalled in the stream (H.264 frame packing SEI, MP4 st3d) is ignored

5 participants